From 25f29045e657c750b03c6f05188d9585a8114bf6 Mon Sep 17 00:00:00 2001 From: Nonimart Date: Tue, 30 Sep 2025 11:45:33 +0200 Subject: [PATCH] FEATURE Optimizing article reader behaviour --- .../css/components/article-revues-toolbar.css | 53 ++++- resources/img/icons/carhop-pause.svg | 6 + resources/img/icons/carhop-stop-reading.svg | 4 + resources/js/singles/reader.ts | 209 ++++++++++++++---- template-parts/articles/article-toolbar.php | 9 +- 5 files changed, 233 insertions(+), 48 deletions(-) create mode 100644 resources/img/icons/carhop-pause.svg create mode 100644 resources/img/icons/carhop-stop-reading.svg diff --git a/resources/css/components/article-revues-toolbar.css b/resources/css/components/article-revues-toolbar.css index baa0828..bca3405 100644 --- a/resources/css/components/article-revues-toolbar.css +++ b/resources/css/components/article-revues-toolbar.css @@ -30,16 +30,63 @@ a.cta--download-pdf { @apply ml-auto; } + + #listen-article, + #stop-reading { + @apply rounded-full w-12 h-12 flex items-center justify-center m-0 p-0 transition-all duration-300; + } #listen-article { - @apply bg-primary text-white rounded-full w-12 h-12 flex items-center justify-center m-0 p-0 transition-all duration-300; + @apply bg-primary text-white; &:hover { @apply scale-110; } img { @apply w-6 h-6; } - &.is-active { - @apply bg-red-500; + &[data-reading-status='playing'] { + /* @apply bg-blue-500; */ + @apply bg-white border border-primary; + /* &:hover { + @apply bg-red-500; + } */ + #play-reading { + @apply hidden; + } + #pause-reading { + @apply block; + } + } + + &[data-reading-status='stopped'] { + #play-reading { + @apply block; + } + #pause-reading { + @apply hidden; + } + } + &[data-reading-status='paused'] { + @apply bg-yellow-500; + #play-reading { + @apply block; + } + #pause-reading { + @apply hidden; + } + } + } + #stop-reading { + @apply bg-primary hidden; + img { + @apply w-4 h-4; + } + } + &:has( + #listen-article[data-reading-status='playing'], + #listen-article[data-reading-status='paused'] + ) { + #stop-reading { + @apply flex; } } } diff --git a/resources/img/icons/carhop-pause.svg b/resources/img/icons/carhop-pause.svg new file mode 100644 index 0000000..cbc3618 --- /dev/null +++ b/resources/img/icons/carhop-pause.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/img/icons/carhop-stop-reading.svg b/resources/img/icons/carhop-stop-reading.svg new file mode 100644 index 0000000..d8c39b4 --- /dev/null +++ b/resources/img/icons/carhop-stop-reading.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/js/singles/reader.ts b/resources/js/singles/reader.ts index 26d51be..80a28cb 100644 --- a/resources/js/singles/reader.ts +++ b/resources/js/singles/reader.ts @@ -1,20 +1,68 @@ -export default function handleArticleReader() { - const button = document.getElementById('listen-article'); - const article = document.querySelector('.article-content'); +declare var Notyf: any; - // Paramètres fixes définis dans le code - const speechSettings = { - rate: 1.2, // Vitesse de lecture (0.1 à 2) - pitch: 1.1, // Tonalité (0 à 2) - volume: 1, // Volume (0 à 1) - }; +/* ******************************************************** + * ******************* CONFIGURATION ******************* + * ******************************************************** + */ + +interface ReaderConfig { + rate: number; + pitch: number; + volume: number; + highlightColor: string; + highlightStyle: string; +} + +const DEFAULT_CONFIG: ReaderConfig = { + rate: 1.2, + pitch: 1.1, + volume: 1, + highlightColor: '#f1fcf9', + highlightStyle: + 'background-color: #f1fcf9; padding: 2px 4px; border-radius: 3px; color: #136F63;', +}; + +export default function handleArticleReader() { + // ✅ DÉCLARATION DES VARIABLES + const playPauseButton = document.getElementById('listen-article'); + const stopReadingButton = document.getElementById('stop-reading'); + const article = document.querySelector('.article-content'); let utterance: SpeechSynthesisUtterance; let voices: SpeechSynthesisVoice[] = []; let thomasVoice: SpeechSynthesisVoice | null = null; + let originalContent = ''; + let words: string[] = []; + let currentWordIndex = 0; + + /* ******************************************************** + * ************** MÉTHODES INTERNES ********************* + * ******************************************************** + */ + + // Fonctions de gestion du bouton + function setReaderButtonReading(): void { + if (!playPauseButton) return; + playPauseButton.setAttribute('data-reading-status', 'playing'); + playPauseButton.setAttribute('title', 'Arrêter la lecture vocale'); + playPauseButton.setAttribute('aria-label', 'Arrêter la lecture vocale'); + } + + function setReaderButtonStopped(): void { + if (!playPauseButton) return; + playPauseButton.setAttribute('data-reading-status', 'stopped'); + playPauseButton.setAttribute('title', "Lancer la lecture vocale de l'article"); + playPauseButton.setAttribute('aria-label', "Lecture vocale de l'article"); + } + function setReaderButtonPaused(): void { + if (!playPauseButton) return; + playPauseButton.setAttribute('data-reading-status', 'paused'); + playPauseButton.setAttribute('title', 'Reprendre la lecture vocale'); + playPauseButton.setAttribute('aria-label', 'Reprendre la lecture vocale'); + } // Charger les voix et trouver Thomas - function loadVoices() { + function loadVoices(): void { voices = speechSynthesis.getVoices(); // Chercher la voix Thomas (peut être "Thomas" ou contenir "Thomas") @@ -29,16 +77,8 @@ export default function handleArticleReader() { } } - // Charger les voix au démarrage et quand elles changent - loadVoices(); - speechSynthesis.onvoiceschanged = loadVoices; - - // Variables pour le highlighting - let originalContent = ''; - let words: string[] = []; - let currentWordIndex = 0; - - function highlightWord(index: number) { + // Fonctions de highlighting + function highlightWord(index: number): void { if (!article || index >= words.length) return; // Restaurer le contenu original si c'est le premier mot @@ -49,7 +89,7 @@ export default function handleArticleReader() { // Créer le nouveau HTML avec le mot highlighted const highlightedWords = words.map((word, i) => { if (i === index) { - return `${word}`; + return `${word}`; } return word; }); @@ -57,12 +97,13 @@ export default function handleArticleReader() { article.innerHTML = highlightedWords.join(' '); } - function removeHighlight() { + function removeHighlight(): void { if (article && originalContent) { article.innerHTML = originalContent; } } + // Création de l'utterance function createUtterance(text: string): SpeechSynthesisUtterance { // Sauvegarder le contenu original et préparer les mots originalContent = article ? article.innerHTML : ''; @@ -72,10 +113,10 @@ export default function handleArticleReader() { utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'fr-FR'; - // Appliquer les paramètres fixes - utterance.rate = speechSettings.rate; - utterance.pitch = speechSettings.pitch; - utterance.volume = speechSettings.volume; + // Appliquer la configuration + utterance.rate = DEFAULT_CONFIG.rate; + utterance.pitch = DEFAULT_CONFIG.pitch; + utterance.volume = DEFAULT_CONFIG.volume; // Utiliser Thomas par défaut if (thomasVoice) { @@ -91,14 +132,14 @@ export default function handleArticleReader() { }; utterance.onend = () => { - button.textContent = '🔊 Lire en vocal'; + setReaderButtonStopped(); removeHighlight(); currentWordIndex = 0; }; utterance.onerror = (event) => { console.error('Erreur lors de la lecture:', event); - button.textContent = '🔊 Lire en vocal'; + setReaderButtonStopped(); removeHighlight(); currentWordIndex = 0; }; @@ -106,21 +147,105 @@ export default function handleArticleReader() { return utterance; } - button?.addEventListener('click', () => { - if (speechSynthesis.speaking) { - speechSynthesis.cancel(); - button.classList.remove('is-active'); - removeHighlight(); // Retirer le highlight quand on arrête - currentWordIndex = 0; - } else { - if (article) { - const text = (article as HTMLElement).innerText.trim(); - if (text) { - createUtterance(text); - speechSynthesis.speak(utterance); - button.classList.add('is-active'); - } + // Fonctions de contrôle de lecture + function playReading(): void { + if (article) { + const text = (article as HTMLElement).innerText.trim(); + if (text) { + createUtterance(text); + speechSynthesis.speak(utterance); + setReaderButtonReading(); + showReaderStartedMessage(); } } + } + + function pauseReading(): void { + speechSynthesis.pause(); + setReaderButtonPaused(); + } + + function stopReading(): void { + speechSynthesis.cancel(); + setReaderButtonStopped(); + removeHighlight(); + currentWordIndex = 0; + } + + /* ******************************************************** + * *************** INITIALISATION ********************** + * ******************************************************** + */ + + // Vérifier si la synthèse vocale est déjà en cours au chargement de la page + if (speechSynthesis.speaking) { + speechSynthesis.cancel(); + setReaderButtonStopped(); + } + + // Arrêter la lecture vocale quand l'utilisateur quitte la page + window.addEventListener('beforeunload', () => { + if (speechSynthesis.speaking) { + speechSynthesis.cancel(); + setReaderButtonStopped(); + } + }); + + // Arrêter la lecture vocale quand l'onglet devient invisible + document.addEventListener('visibilitychange', () => { + if (document.hidden && speechSynthesis.speaking) { + speechSynthesis.pause(); + } else if (!document.hidden && speechSynthesis.paused) { + speechSynthesis.resume(); + setReaderButtonReading(); + } + }); + + // Charger les voix au démarrage et quand elles changent + loadVoices(); + speechSynthesis.onvoiceschanged = loadVoices; + + // Event listener principal + playPauseButton?.addEventListener('click', () => { + if (speechSynthesis.speaking && !speechSynthesis.paused) { + // En cours de lecture → Pause + pauseReading(); + } else if (speechSynthesis.paused) { + // En pause → Reprendre + speechSynthesis.resume(); + setReaderButtonReading(); + } else { + // Arrêté → Démarrer + playReading(); + } + }); + stopReadingButton?.addEventListener('click', () => { + stopReading(); + setReaderButtonStopped(); }); } + +function showReaderStartedMessage() { + const notyf = new Notyf({ + duration: 2000, + ripple: false, + dismissible: true, + types: [ + { + type: 'success', + background: '#10B981', + icon: { + className: 'notyf__icon--success', + tagName: 'i', + text: '🔊', + }, + }, + ], + position: { + x: 'right', + y: 'top', + }, + }); + + notyf.success(`Lecture de l'article lancée !`); +} diff --git a/template-parts/articles/article-toolbar.php b/template-parts/articles/article-toolbar.php index aa96caf..1ff7a9c 100644 --- a/template-parts/articles/article-toolbar.php +++ b/template-parts/articles/article-toolbar.php @@ -47,9 +47,12 @@ $pdf_url = isset($pdf_version) && !empty($pdf_version['url']) ? $pdf_version['ur - +