From a9765891ff073f7149f9be728167800bc4a8e437 Mon Sep 17 00:00:00 2001 From: Nonimart Date: Mon, 29 Sep 2025 15:53:30 +0200 Subject: [PATCH] FEATURE Updating speech reading article --- .../css/components/article-revues-toolbar.css | 16 ++ resources/img/icons/carhop-ecouter.svg | 14 ++ resources/js/singles/article-toolbar.ts | 5 +- resources/js/singles/reader.ts | 137 +++++++++++------- single-articles.php | 11 -- template-parts/articles/article-toolbar.php | 22 ++- 6 files changed, 136 insertions(+), 69 deletions(-) create mode 100644 resources/img/icons/carhop-ecouter.svg diff --git a/resources/css/components/article-revues-toolbar.css b/resources/css/components/article-revues-toolbar.css index e6afe39..1c93ba6 100644 --- a/resources/css/components/article-revues-toolbar.css +++ b/resources/css/components/article-revues-toolbar.css @@ -30,5 +30,21 @@ a.cta--download-pdf { @apply ml-auto; } + #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; + &:hover { + @apply scale-110; + } + img { + @apply w-6 h-6; + } + &.is-active { + @apply bg-red-500; + + } + } + + .toolbar-actions { + @apply flex items-center gap-3 ml-auto; } } diff --git a/resources/img/icons/carhop-ecouter.svg b/resources/img/icons/carhop-ecouter.svg new file mode 100644 index 0000000..c2e3ce3 --- /dev/null +++ b/resources/img/icons/carhop-ecouter.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/resources/js/singles/article-toolbar.ts b/resources/js/singles/article-toolbar.ts index 3690710..b56145b 100644 --- a/resources/js/singles/article-toolbar.ts +++ b/resources/js/singles/article-toolbar.ts @@ -3,7 +3,7 @@ export function handleArticleToolbar() { } function observeTabsButtons(): void { - const toolbarButtons = document.querySelectorAll('#article-toolbar button'); + const toolbarButtons = document.querySelectorAll('#article-toolbar button[role="tab"]'); toolbarButtons.forEach((toolbarButton) => { toolbarButton.addEventListener('click', () => { @@ -20,7 +20,7 @@ function toggleActiveTab(toolbarButton: HTMLElement): void { } function resetActiveToolbarButtons(): void { - const toolbarButtons = document.querySelectorAll('#article-toolbar button'); + const toolbarButtons = document.querySelectorAll('#article-toolbar button[role="tab"]'); toolbarButtons.forEach((toolbarButton) => { toolbarButton.setAttribute('aria-selected', 'false'); }); @@ -30,4 +30,5 @@ function handleActiveTabContent(tab: string): void { const contentWrapper = document.querySelector('.content-wrapper'); contentWrapper?.setAttribute('data-active-tab', tab); console.log(tab); + console.log('contentWrapper'); } diff --git a/resources/js/singles/reader.ts b/resources/js/singles/reader.ts index 867f0aa..26d51be 100644 --- a/resources/js/singles/reader.ts +++ b/resources/js/singles/reader.ts @@ -1,29 +1,31 @@ export default function handleArticleReader() { - const button = document.getElementById('speak-btn'); + const button = document.getElementById('listen-article'); const article = document.querySelector('.article-content'); - // Contrôles de paramètres (optionnels) - const rateControl = document.getElementById('speech-rate') as HTMLInputElement; - const pitchControl = document.getElementById('speech-pitch') as HTMLInputElement; - const volumeControl = document.getElementById('speech-volume') as HTMLInputElement; - const voiceSelect = document.getElementById('speech-voice') as HTMLSelectElement; + // 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) + }; let utterance: SpeechSynthesisUtterance; let voices: SpeechSynthesisVoice[] = []; + let thomasVoice: SpeechSynthesisVoice | null = null; - // Charger les voix disponibles + // Charger les voix et trouver Thomas function loadVoices() { voices = speechSynthesis.getVoices(); - if (voiceSelect && voices.length > 0) { - voiceSelect.innerHTML = ''; - voices - .filter((voice) => voice.lang.startsWith('fr')) // Filtrer les voix françaises - .forEach((voice, index) => { - const option = document.createElement('option'); - option.value = index.toString(); - option.textContent = `${voice.name} (${voice.lang})`; - voiceSelect.appendChild(option); - }); + + // Chercher la voix Thomas (peut être "Thomas" ou contenir "Thomas") + thomasVoice = + voices.find( + (voice) => voice.name.toLowerCase().includes('thomas') || voice.name === 'Thomas' + ) || null; + + // Si Thomas n'est pas trouvé, prendre une voix française par défaut + if (!thomasVoice) { + thomasVoice = voices.find((voice) => voice.lang.startsWith('fr')) || null; } } @@ -31,59 +33,94 @@ export default function handleArticleReader() { loadVoices(); speechSynthesis.onvoiceschanged = loadVoices; - // Configuration par défaut - const defaultSettings = { - rate: 0.9, // Vitesse (0.1 à 10) - pitch: 1, // Tonalité (0 à 2) - volume: 1, // Volume (0 à 1) - voice: null, // Voix (null = voix par défaut) - }; + // Variables pour le highlighting + let originalContent = ''; + let words: string[] = []; + let currentWordIndex = 0; + + function highlightWord(index: number) { + if (!article || index >= words.length) return; + + // Restaurer le contenu original si c'est le premier mot + if (index === 0) { + article.innerHTML = originalContent; + } + + // Créer le nouveau HTML avec le mot highlighted + const highlightedWords = words.map((word, i) => { + if (i === index) { + return `${word}`; + } + return word; + }); + + article.innerHTML = highlightedWords.join(' '); + } + + function removeHighlight() { + if (article && originalContent) { + article.innerHTML = originalContent; + } + } function createUtterance(text: string): SpeechSynthesisUtterance { + // Sauvegarder le contenu original et préparer les mots + originalContent = article ? article.innerHTML : ''; + words = text.split(/\s+/).filter((word) => word.trim().length > 0); + currentWordIndex = 0; + utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'fr-FR'; - // Appliquer les paramètres depuis les contrôles ou les valeurs par défaut - utterance.rate = rateControl ? parseFloat(rateControl.value) : defaultSettings.rate; - utterance.pitch = pitchControl ? parseFloat(pitchControl.value) : defaultSettings.pitch; - utterance.volume = volumeControl ? parseFloat(volumeControl.value) : defaultSettings.volume; + // Appliquer les paramètres fixes + utterance.rate = speechSettings.rate; + utterance.pitch = speechSettings.pitch; + utterance.volume = speechSettings.volume; - // Sélectionner la voix - if (voiceSelect && voices.length > 0) { - const selectedVoiceIndex = parseInt(voiceSelect.value); - const frenchVoices = voices.filter((voice) => voice.lang.startsWith('fr')); - if (frenchVoices[selectedVoiceIndex]) { - utterance.voice = frenchVoices[selectedVoiceIndex]; - } + // Utiliser Thomas par défaut + if (thomasVoice) { + utterance.voice = thomasVoice; } + // Event pour highlighter les mots pendant la lecture + utterance.onboundary = (event) => { + if (event.name === 'word') { + highlightWord(currentWordIndex); + currentWordIndex++; + } + }; + utterance.onend = () => { button.textContent = '🔊 Lire en vocal'; + removeHighlight(); + currentWordIndex = 0; }; utterance.onerror = (event) => { console.error('Erreur lors de la lecture:', event); button.textContent = '🔊 Lire en vocal'; + removeHighlight(); + currentWordIndex = 0; }; return utterance; } - if (button) { - button.addEventListener('click', () => { - if (speechSynthesis.speaking) { - speechSynthesis.cancel(); - button.textContent = '🔊 Lire en vocal'; - } else { - if (article) { - const text = article.innerText.trim(); - if (text) { - createUtterance(text); - speechSynthesis.speak(utterance); - button.textContent = '⏹️ Arrêter la lecture'; - } + 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'); } } - }); - } + } + }); } diff --git a/single-articles.php b/single-articles.php index 6b42793..9f986b9 100644 --- a/single-articles.php +++ b/single-articles.php @@ -22,18 +22,7 @@ $revueID = get_field('related_revue', get_the_ID());
- - - - - - - - - - - get_the_ID() )); ?> diff --git a/template-parts/articles/article-toolbar.php b/template-parts/articles/article-toolbar.php index 0903540..f6bf11e 100644 --- a/template-parts/articles/article-toolbar.php +++ b/template-parts/articles/article-toolbar.php @@ -37,11 +37,21 @@ $pdf_url = isset($pdf_version) ? $pdf_version['url'] : null; Informations - - - - PDF - - +
+ + + + + PDF + + + + + +
+
\ No newline at end of file