PageSourceSearch

https://www.lumovivo.org/msg/favoris.js

js lumovivo.org collected 2026-10-02 00:40:04 UTC 16,788 bytes, 450 lines download raw bytes

1/*
2                                                                                      
3    _/        _/    _/  _/      _/    _/_/    _/      _/   _/   _/      _/    _/_/    
4   _/        _/    _/  _/_/  _/_/  _/    _/  _/      _/   _/   _/      _/  _/    _/   
5  _/        _/    _/  _/  _/  _/  _/    _/  _/      _/   _/   _/      _/  _/    _/    
6 _/        _/    _/  _/      _/  _/    _/    _/  _/     _/     _/  _/    _/    _/     
7_/_/_/_/    _/_/    _/      _/    _/_/        _/       _/       _/        _/_/
8
9*/
10
11document.addEventListener('DOMContentLoaded', () => {
12
13  function getHistoryLimit() {
14   const limit = parseInt(localStorage.getItem('historyLimit'), 10);
15   if (isNaN(limit)) return 20;
16   return Math.min(Math.max(limit, 1), 100);
17  }
18
19  const historyLimitInput = document.getElementById('history-limit');
20  if (historyLimitInput) {
21   historyLimitInput.value = getHistoryLimit();
22   historyLimitInput.addEventListener('change', () => {
23    const newLimit = parseInt(historyLimitInput.value, 10) || 20;
24    localStorage.setItem('historyLimit', newLimit);
25    history = loadFavorites();
26    updateHistoryCount();
27   });
28  }
29
30  // Fonction pour charger les favoris
31  const favoritesDiv = document.getElementById('favoris-list');
32  function loadFavorites() {
33   const favorites = [];
34   for (let i = 0; i < localStorage.length; i++) {
35    const key = localStorage.key(i);
36    if (key.startsWith('fav_')) {
37     try {
38      const fav = JSON.parse(localStorage.getItem(key));
39      if (fav && fav.quote) {
40       fav.key = key;
41       favorites.push(fav);
42      }
43     } catch (err) {
44      localStorage.removeItem(key); // nettoyage
45     }
46    }
47   }
48   return favorites;
49  }
50
51  let favorites = loadFavorites();
52  let currentUtterance = null;
53
54  let availableVoices = speechSynthesis.getVoices();
55  speechSynthesis.onvoiceschanged = () => {
56   availableVoices = speechSynthesis.getVoices();
57  };
58
59  // Fonction pour ajouter au titre le nombre de favoris
60  function updateTitle() {
61  const titleElement = document.querySelector('h3');
62  titleElement.innerHTML = `${favorites.length} Favoris`;
63  }
64  updateTitle();
65
66  // Fonction pour mettre à jour le compteur de favoris
67  function updateFavoriteCount() {
68   const favoriteCountElement = document.getElementById('favoris-count');
69   if (favoriteCountElement) {
70    favoriteCountElement.innerText = favorites.length;
71   }
72  }
73
74  // Fonction pour mettre à jour le compteur de note
75  function updateNoteCount() {
76   const noteCountElement = document.getElementById('notes-count');
77   let count = 0;
78   for (let i = 0; i < localStorage.length; i++) {
79    const key = localStorage.key(i);
80    if (key.startsWith('note_')) count++;
81   }
82   noteCountElement.innerText = count;
83  }
84
85  // Fonction pour rendre les favoris
86  function renderFavorites() {
87   if (favorites.length > 0) {
88    const sortedFavorites = [...favorites].sort((a, b) => new Date(b.date) - new Date(a.date));
89    favoritesDiv.innerHTML = sortedFavorites.map(f => {
90     const favKey = f.key;
91     const noteKey = getNoteKey(favKey);
92     const noteStored = localStorage.getItem(noteKey);
93     const noteText = noteStored ? JSON.parse(noteStored).note : '';
94     return `
95      <p>
96      <span class="bulo">☀</span> 
97       <a href="${f.url}" class="theme roll" data-key="${favKey}">
98        <span>[${f.theme}]</span></a>
99       ${f.quote}
100       <span class="opt-but">
101        <span data-quote="${f.key}" class="listen" title="Écouter cette citation" aria-label="Écouter la Citation">
102         <i class="fas fa-headphones"></i>
103        </span>
104        <span data-quote="${f.key}" class="remove" title="Supprimer ce Favori" aria-label="Supprimer ce Favori">
105         <i class="far fa-trash-can"></i>
106        </span>
107        <span data-quote="${f.key}" class="note" title="Lire/éditer la Note" aria-label="Lire/éditer la Note">
108         <i id="note-icon-${f.key}" class="${noteText ? 'fas' : 'far'} fa-note-sticky"></i>
109        </span>
110       </span>
111      </p>
112      `;
113     }).join('');
114
115     // Envoyer le thème et la citation sur la page html
116     favoritesDiv.querySelectorAll('a.theme').forEach(link => {
117      const favKey = link.dataset.key;
118      const f = favorites.find(fav => fav.key === favKey);
119      link.addEventListener('click', e => {
120       e.preventDefault();
121       const themeUrl = `${f.url}?quote=${encodeURIComponent(f.quote)}`;
122       window.location.href = themeUrl;
123      });
124     });
125
126    } else {
127     favoritesDiv.innerHTML = '<p id="message"><span style="color:#d9534f;font-size:14px;padding:5px;background-color:#f8d7da;border-radius:4px;">Aucun favori n\'a encore été enregistré. Ce module permet de gérer les favoris et leurs notes associées.</span></p>';
128    }
129
130    // Ajouter les gestionnaires d'événements pour la note
131    document.querySelectorAll('.note').forEach((icon) => {
132     icon.addEventListener('click', (e) => {
133      const favKey = e.target.closest('.note').getAttribute('data-quote');
134      openNoteModal(e, favKey); 
135     });
136    });
137   }
138
139   // Mise à jour de l'icône de la note pour chaque favori
140   function updateNoteIcon(favoriteQuote, favKey) {
141    const noteIcon = document.getElementById(`note-icon-${favKey}`);
142    if (!noteIcon) return;
143    const noteExists = localStorage.getItem('note_' + favKey.slice(4)); 
144    noteIcon.classList.toggle('fas', !!noteExists);
145    noteIcon.classList.toggle('far', !noteExists);
146   }
147
148   renderFavorites();
149   updateFavoriteCount();
150   updateNoteCount();
151   updateHistoryCount();
152
153   // Gestion des clics sur la liste de favoris
154   favoritesDiv.addEventListener('click', (e) => {
155   // Supprimer un favori
156   if (e.target.closest('.remove')) {
157    const btn = e.target.closest('.remove');
158    const favKey = btn.getAttribute('data-quote'); 
159
160    // Confirmation avant suppression
161    const confirmDelete = confirm(
162     "Retirer cette citation de vos favoris ? \n- La note associée restera enregistrée dans le cache."
163    );
164    if (!confirmDelete) return;
165
166    const favoriteQuote = favorites.find(f => f.key === favKey);
167    if (favoriteQuote) {
168     if (currentUtterance && currentUtterance._quote === favoriteQuote.quote) {
169      speechSynthesis.cancel();
170      currentUtterance = null;
171      document.querySelectorAll('.listen i').forEach(i => i.classList.remove('blinking'));
172     }
173     // Retirer le favori de la liste
174     favorites = favorites.filter(f => f.key !== favKey);
175     localStorage.removeItem(favKey);
176
177     // Mise à jour compteur global favori
178     fetch('/msg/favoris.php', {
179      method: 'POST',
180      headers: { 'Content-Type': 'application/json' },
181      body: JSON.stringify({
182       action: 'decrement', 
183       quote: favoriteQuote.quote,
184       theme: favoriteQuote.theme,
185       url: favoriteQuote.url.replace('https://www.lumovivo.org/msg/', ''),
186       date: favoriteQuote.date
187      })
188     })
189     .then(res => res.json())
190     .then(data => {
191      const countEl = document.getElementById('fav-util-count');
192      if (countEl) countEl.innerText = data.count || 0;
193     })
194     .catch(err => console.error(err));
195    }
196
197    renderFavorites();
198    updateTitle();
199    updateFavoriteCount();
200    updateHistoryCount();
201   }
202
203   // Écouter un favori
204   if (e.target.closest('.listen')) {
205    const btn = e.target.closest('.listen');
206    if (!btn) return; 
207    const favKey = btn.getAttribute('data-quote');
208    const favoriteQuote = favorites.find(f => f.key === favKey);
209    const textToRead = favoriteQuote.quote;
210    const icon = btn.querySelector('i');
211    if (currentUtterance && currentUtterance._quote === textToRead) {
212     if (speechSynthesis.speaking && !speechSynthesis.paused) {
213      speechSynthesis.pause();
214      icon.classList.remove('blinking');
215     } else if (speechSynthesis.paused) {
216      speechSynthesis.resume();
217      icon.classList.add('blinking');
218     }
219    } else {
220     if (speechSynthesis.speaking || speechSynthesis.paused) {
221      speechSynthesis.cancel();
222      document.querySelectorAll('.listen i').forEach(i => i.classList.remove('blinking'));
223     }
224     currentUtterance = new SpeechSynthesisUtterance(textToRead);
225     currentUtterance._quote = textToRead;
226     currentUtterance.lang = 'fr-FR';
227     currentUtterance.rate = 1;
228     currentUtterance.pitch = 1;
229     // Sélection de la voix comme dans citations.js
230     let chosenVoice = availableVoices.find(v =>
231      [
232       "Microsoft Julie - French (France)",
233       "Microsoft Hortense - French (France)",
234       "Microsoft Paul - French (France)",
235       "Microsoft Matthieu - French (France)",
236       "Google français (femelle)",
237       "Google français (male)",
238       "Google français"
239      ].includes(v.name)
240     );
241     if (!chosenVoice) chosenVoice = availableVoices.find(v => v.lang.startsWith("fr"));
242     if (chosenVoice) currentUtterance.voice = chosenVoice;
243     currentUtterance.onstart = () => {
244      document.querySelectorAll('.listen i').forEach(i => i.classList.remove('blinking'));
245      icon.classList.add('blinking');
246     };
247     currentUtterance.onend = () => {
248      icon.classList.remove('blinking');
249      currentUtterance = null;
250     };
251     speechSynthesis.speak(currentUtterance);
252    }
253   }
254  });
255
256  // Annuler toute lecture lors du rechargement ou de la fermeture
257  window.addEventListener('beforeunload', () => {
258   if (speechSynthesis.speaking || speechSynthesis.paused) {
259    speechSynthesis.cancel();
260   }
261  });
262
263  // Récupération de la clé de note
264  function getNoteKey(favKey) {
265   return 'note_' + favKey.slice(4);
266  }
267
268  // Fonction pour charger la note depuis localStorage
269  function loadNoteFromLocalStorage(favoriteQuote, index) {
270   const noteKey = getNoteKey(favoriteQuote.key);
271   const stored = localStorage.getItem(noteKey);
272   const textarea = document.getElementById(`note-text-${index}`);
273   if (textarea) {
274    if (stored) {
275     try {
276      const parsed = JSON.parse(stored);
277      textarea.value = parsed.note || '';
278     } catch (err) {
279      textarea.value = stored;
280     }
281    } else {
282     textarea.value = '';
283    }
284   }
285  }
286
287  // Gestion de la modale éditable
288  function openNoteModal(e, favKey) {
289   const favoriteQuote = favorites.find(f => f.key === favKey); 
290   const index = favorites.indexOf(favoriteQuote);
291   const quoteElement = e.target.closest('.note');
292   if (!quoteElement) return;
293   loadNoteFromLocalStorage(favoriteQuote, index);
294   // Création de la fenêtre modale
295   const modal = document.createElement('div');
296   modal.id = 'modal';
297   // Contenu de la fenêtre modale
298   const modalContent = document.createElement('div');
299   modalContent.classList.add('modal-content');
300   // Titre de la fenêtre modale
301   const modalTitle = document.createElement('h4');
302   modalTitle.classList.add('modal-title');
303   modalTitle.textContent = 'Note associée';
304   const modalQuote = document.createElement('div');
305   modalQuote.classList.add('modal-quote');
306   modalQuote.textContent = favoriteQuote.quote;
307   const storedNote = localStorage.getItem(getNoteKey(favoriteQuote.key));
308   const textarea = document.createElement('textarea');
309   textarea.classList.add('modal-textarea');
310   textarea.rows = 5;
311   textarea.placeholder = 'Entrez votre note ici...';
312   textarea.maxLength = 1000;
313   textarea.value = storedNote ? JSON.parse(storedNote).note : '';
314   ['keydown', 'contextmenu', 'selectstart'].forEach(eventType => {
315    textarea.addEventListener(eventType, function(e) {
316    e.stopPropagation();
317    });
318   });
319   const btnContainer = document.createElement('div');
320   btnContainer.classList.add('modal-btn-container');
321   // Bouton "Supprimer"
322   const deleteBtn = document.createElement('button');
323   deleteBtn.classList.add('modal-btn-delete');
324   deleteBtn.innerHTML = '<i class="far fa-trash-can"></i><span class="modal-btn-txt"> Supprimer</span>';
325   deleteBtn.setAttribute('title', 'Supprimer définitivement cette note');
326   deleteBtn.setAttribute('aria-label', 'Supprimer la note');
327   deleteBtn.addEventListener('click', function () {
328    const noteKey = getNoteKey(favoriteQuote.key);
329    localStorage.removeItem(noteKey); 
330    renderFavorites();
331    updateNoteIcon(favoriteQuote, favKey);
332    updateNoteCount();
333    document.body.removeChild(modal);
334   });
335   // Bouton "Fermer
336   const closeBtn = document.createElement('button');
337   closeBtn.classList.add('modal-btn-close');
338   closeBtn.textContent = 'Annuler';
339   closeBtn.setAttribute('title', 'Abandonner les modifications');
340   closeBtn.setAttribute('aria-label', 'Annuler et fermer la fenêtre');
341   closeBtn.addEventListener('click', () =>
342    document.body.removeChild(modal)
343   );
344   // Bouton "Sauvegarder"
345   const saveBtn = document.createElement('button');
346   saveBtn.classList.add('modal-btn-save');
347   saveBtn.innerHTML = '<i class="fas fa-check"></i> Enregistrer';
348   saveBtn.setAttribute('title', 'Sauvegarder les changements'); 
349   saveBtn.setAttribute('aria-label', 'Enregistrer la note et fermer');
350   saveBtn.addEventListener('click', () => {
351    const noteKey = getNoteKey(favoriteQuote.key);
352    const noteText = textarea.value.trim();
353    if (noteText) {
354     localStorage.setItem(noteKey, JSON.stringify({
355      quote: favoriteQuote.quote,
356      note: noteText,
357      theme: favoriteQuote.theme,
358      url: favoriteQuote.url.replace('https://www.lumovivo.org/msg/', ''),
359      date: new Date().toISOString() 
360     }));
361    } else {
362     localStorage.removeItem(noteKey);
363    }
364    renderFavorites();
365    updateNoteIcon(favoriteQuote, favKey);
366    updateNoteCount();
367    document.body.removeChild(modal);
368   });
369   // Ajout des boutons dans le conteneur
370   btnContainer.appendChild(deleteBtn);
371   btnContainer.appendChild(closeBtn);
372   btnContainer.appendChild(saveBtn);
373   // Ajout du contenu à la modale
374   modalContent.appendChild(modalTitle);
375   modalContent.appendChild(modalQuote);
376   modalContent.appendChild(textarea);
377   modalContent.appendChild(btnContainer);
378   modal.appendChild(modalContent);
379   document.body.appendChild(modal);
380   modal.style.display = 'flex';
381   textarea.focus();
382   textarea.setSelectionRange(0, 0);
383   textarea.addEventListener('keydown', e => {
384    if (e.ctrlKey && e.key === 'a') {
385     e.preventDefault();
386     textarea.select();
387    }
388   });
389   // Fermeture de la modale si l'utilisateur clique en dehors
390   modal.addEventListener('click', function (e) {
391    if (e.target === modal) {
392     document.body.removeChild(modal);
393    }
394   });
395   // Fermeture de la modale au pressage de la touche "Échap"
396   function escHandler(e) {
397    if (e.key === 'Escape') {
398     document.body.removeChild(modal);
399     document.removeEventListener('keydown', escHandler, true);
400    }
401   }
402   document.addEventListener('keydown', escHandler, true);
403  }
404
405  renderFavorites();
406  updateTitle();
407  updateNoteCount();
408  updateFavoriteCount();
409  updateHistoryCount();
410
411  // Préchargement QR et footer pour impression
412  const qrImageSrc = '/images/limpensableqr.webp';
413  const preloadedQr = new Image();
414  preloadedQr.src = qrImageSrc;
415
416  const printFooter = document.createElement('div');
417  printFooter.id = 'print-footer';
418  printFooter.style.display = 'none';
419  favoritesDiv.insertAdjacentElement('afterend', printFooter);
420
421  const copyrightEl = document.createElement('div');
422  copyrightEl.id = 'print-copyright';
423  copyrightEl.textContent = "Lumovivo.org © 1981-2030 Tous droits réservés \nLUMOVIVO™ – Daniel Vallat – DAD Editions \nTigre Concept International Communications";
424  copyrightEl.style.color = "#efefef";
425  copyrightEl.style.fontSize = "0.8em";
426  copyrightEl.style.textAlign = "center";
427  copyrightEl.style.lineHeight = "1.0em";
428  copyrightEl.style.margin = "20px 0";
429  copyrightEl.style.whiteSpace = "pre";
430  printFooter.appendChild(copyrightEl);
431
432  const qrImg = document.createElement('img');
433  qrImg.id = 'print-qr';
434  qrImg.src = qrImageSrc;
435  qrImg.style.display = 'block';
436  qrImg.style.margin = '0 auto';
437  qrImg.style.width = '350px';
438  qrImg.style.height = '520px';
439  printFooter.appendChild(qrImg);
440
441  window.addEventListener('beforeprint', () => {
442   document.body.classList.add('favoris-page-print');
443   if (printFooter) printFooter.style.display = 'block';
444  });
445  window.addEventListener('afterprint', () => {
446   document.body.classList.remove('favoris-page-print');
447   if (printFooter) printFooter.style.display = 'none';
448  });
449
450});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.