FEATURE Optimizing article reader behaviour

This commit is contained in:
Nonimart 2025-09-30 11:45:33 +02:00
parent 38c5184203
commit 25f29045e6
5 changed files with 233 additions and 48 deletions

View File

@ -30,16 +30,63 @@
a.cta--download-pdf { a.cta--download-pdf {
@apply ml-auto; @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 { #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 { &:hover {
@apply scale-110; @apply scale-110;
} }
img { img {
@apply w-6 h-6; @apply w-6 h-6;
} }
&.is-active { &[data-reading-status='playing'] {
@apply bg-red-500; /* @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;
} }
} }
} }

View File

@ -0,0 +1,6 @@
<svg width="16" height="23" viewBox="0 0 16 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1459_14367)">
<path d="M0.941406 0V23" stroke="#136F63" stroke-width="2"/>
<path d="M15.0586 0V23" stroke="#136F63" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<rect width="16" height="16" style="fill: #fff;"/>
</svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@ -1,20 +1,68 @@
export default function handleArticleReader() { declare var Notyf: any;
const button = document.getElementById('listen-article');
const article = document.querySelector('.article-content');
// Paramètres fixes définis dans le code /* ********************************************************
const speechSettings = { * ******************* CONFIGURATION *******************
rate: 1.2, // Vitesse de lecture (0.1 à 2) * ********************************************************
pitch: 1.1, // Tonalité (0 à 2) */
volume: 1, // Volume (0 à 1)
}; 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 utterance: SpeechSynthesisUtterance;
let voices: SpeechSynthesisVoice[] = []; let voices: SpeechSynthesisVoice[] = [];
let thomasVoice: SpeechSynthesisVoice | null = null; 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 // Charger les voix et trouver Thomas
function loadVoices() { function loadVoices(): void {
voices = speechSynthesis.getVoices(); voices = speechSynthesis.getVoices();
// Chercher la voix Thomas (peut être "Thomas" ou contenir "Thomas") // 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 // Fonctions de highlighting
loadVoices(); function highlightWord(index: number): void {
speechSynthesis.onvoiceschanged = loadVoices;
// Variables pour le highlighting
let originalContent = '';
let words: string[] = [];
let currentWordIndex = 0;
function highlightWord(index: number) {
if (!article || index >= words.length) return; if (!article || index >= words.length) return;
// Restaurer le contenu original si c'est le premier mot // 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 // Créer le nouveau HTML avec le mot highlighted
const highlightedWords = words.map((word, i) => { const highlightedWords = words.map((word, i) => {
if (i === index) { if (i === index) {
return `<span class="speech-highlight" style="background-color: yellow; padding: 2px 4px; border-radius: 3px;">${word}</span>`; return `<span class="speech-highlight" style="${DEFAULT_CONFIG.highlightStyle}">${word}</span>`;
} }
return word; return word;
}); });
@ -57,12 +97,13 @@ export default function handleArticleReader() {
article.innerHTML = highlightedWords.join(' '); article.innerHTML = highlightedWords.join(' ');
} }
function removeHighlight() { function removeHighlight(): void {
if (article && originalContent) { if (article && originalContent) {
article.innerHTML = originalContent; article.innerHTML = originalContent;
} }
} }
// Création de l'utterance
function createUtterance(text: string): SpeechSynthesisUtterance { function createUtterance(text: string): SpeechSynthesisUtterance {
// Sauvegarder le contenu original et préparer les mots // Sauvegarder le contenu original et préparer les mots
originalContent = article ? article.innerHTML : ''; originalContent = article ? article.innerHTML : '';
@ -72,10 +113,10 @@ export default function handleArticleReader() {
utterance = new SpeechSynthesisUtterance(text); utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'fr-FR'; utterance.lang = 'fr-FR';
// Appliquer les paramètres fixes // Appliquer la configuration
utterance.rate = speechSettings.rate; utterance.rate = DEFAULT_CONFIG.rate;
utterance.pitch = speechSettings.pitch; utterance.pitch = DEFAULT_CONFIG.pitch;
utterance.volume = speechSettings.volume; utterance.volume = DEFAULT_CONFIG.volume;
// Utiliser Thomas par défaut // Utiliser Thomas par défaut
if (thomasVoice) { if (thomasVoice) {
@ -91,14 +132,14 @@ export default function handleArticleReader() {
}; };
utterance.onend = () => { utterance.onend = () => {
button.textContent = '🔊 Lire en vocal'; setReaderButtonStopped();
removeHighlight(); removeHighlight();
currentWordIndex = 0; currentWordIndex = 0;
}; };
utterance.onerror = (event) => { utterance.onerror = (event) => {
console.error('Erreur lors de la lecture:', event); console.error('Erreur lors de la lecture:', event);
button.textContent = '🔊 Lire en vocal'; setReaderButtonStopped();
removeHighlight(); removeHighlight();
currentWordIndex = 0; currentWordIndex = 0;
}; };
@ -106,21 +147,105 @@ export default function handleArticleReader() {
return utterance; return utterance;
} }
button?.addEventListener('click', () => { // Fonctions de contrôle de lecture
if (speechSynthesis.speaking) { function playReading(): void {
speechSynthesis.cancel(); if (article) {
button.classList.remove('is-active'); const text = (article as HTMLElement).innerText.trim();
removeHighlight(); // Retirer le highlight quand on arrête if (text) {
currentWordIndex = 0; createUtterance(text);
} else { speechSynthesis.speak(utterance);
if (article) { setReaderButtonReading();
const text = (article as HTMLElement).innerText.trim(); showReaderStartedMessage();
if (text) {
createUtterance(text);
speechSynthesis.speak(utterance);
button.classList.add('is-active');
}
} }
} }
}
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 !`);
}

View File

@ -47,9 +47,12 @@ $pdf_url = isset($pdf_version) && !empty($pdf_version['url']) ? $pdf_version['ur
<?php endif; ?> <?php endif; ?>
<button id="listen-article" type="button"> <button id="listen-article" type="button" data-reading-status="stopped" title="Lancer la lecture vocale de l'article">
<img class="icon" src="<?php echo get_stylesheet_directory_uri(); ?>/resources/img/icons/carhop-ecouter.svg" alt="Lecture vocale de l'article"> <img id="play-reading" class="icon" src="<?php echo get_stylesheet_directory_uri(); ?>/resources/img/icons/carhop-ecouter.svg" alt="Lecture vocale de l'article">
<img id="pause-reading" class="icon" src="<?php echo get_stylesheet_directory_uri(); ?>/resources/img/icons/carhop-pause.svg" alt="Arrêter la lecture vocale de l'article">
</button>
<button id="stop-reading" type="button" title="Arrêter la lecture vocale de l'article">
<img class="icon" src="<?php echo get_stylesheet_directory_uri(); ?>/resources/img/icons/carhop-stop-reading.svg" alt="Arrêter la lecture vocale de l'article">
</button> </button>
</div> </div>