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.