1jQuery(document).ready(function($) { 2 var playButton = $('#leitor-de-texto-play-button'); 3 var pauseButton = $('#leitor-de-texto-pause-button'); 4 var stopButton = $('#leitor-de-texto-stop-button'); 5 var rateControl = $('.leitor-de-texto-rate-control'); 6 var rateInput = $('#leitor-de-texto-rate'); 7 var rateValueSpan = $('#leitor-de-texto-rate-value'); 8 var readingTimeSpan = $('#leitor-de-texto-reading-time'); // Novo elemento para o tempo de leitura 9 10 var isSpeaking = false; 11 var isPaused = false; 12 var currentUtterance = null; 13 var selectedVoice = null; 14 var speechQueue = []; 15 var fullContentText = ''; // Para armazenar o texto completo após a limpeza 16 17 // Define a velocidade inicial no input e no span 18 rateInput.val(leitorDeTextoData.defaultRate); 19 rateValueSpan.text(leitorDeTextoData.defaultRate); 20 21 // Evento para atualizar o valor da velocidade no span e no utterance (se estiver falando) 22 rateInput.on('input', function() { 23 rateValueSpan.text($(this).val()); 24 if (currentUtterance) { 25 currentUtterance.rate = parseFloat($(this).val()); 26 } 27 }); 28 29 // Verifica se a API SpeechSynthesis é suportada pelo navegador 30 if (!('speechSynthesis' in window)) { 31 console.warn('Leitor de Texto: Seu navegador não suporta a API de Text-to-Speech. Os botões serão ocultados.'); 32 $('.leitor-de-texto-controls').hide(); // Esconde todos os controles se não houver suporte 33 return; // Sai da função se não houver suporte 34 } 35 36 // --- Funções Auxiliares --- 37 38 // Função para obter a voz feminina em português do Brasil 39 function getBrazilianFemaleVoice() { 40 var voices = window.speechSynthesis.getVoices(); 41 var femaleVoice = null; 42 var defaultLang = leitorDeTextoData.language.replace('_', '-'); // Ex: 'pt-BR' 43 44 // 1. Tenta encontrar uma voz explicitamente feminina em pt-BR 45 for (var i = 0; i < voices.length; i++) { 46 if (voices[i].lang === defaultLang && voices[i].name.toLowerCase().includes('female')) { 47 femaleVoice = voices[i]; 48 break; 49 } 50 } 51 52 // 2. Se não encontrar uma feminina, tenta qualquer voz em pt-BR 53 if (!femaleVoice) { 54 for (var i = 0; i < voices.length; i++) { 55 if (voices[i].lang === defaultLang) { 56 femaleVoice = voices[i]; 57 break; 58 } 59 } 60 } 61 62 // 3. Se ainda não encontrar pt-BR, tenta qualquer voz que comece com 'pt' 63 if (!femaleVoice) { 64 var langPrefix = defaultLang.substring(0, 2); 65 for (var i = 0; i < voices.length; i++) { 66 if (voices[i].lang.startsWith(langPrefix)) { 67 femaleVoice = voices[i]; 68 break; 69 } 70 } 71 } 72 73 // 4. Como último recurso, retorna a primeira voz disponÃvel 74 if (!femaleVoice && voices.length > 0) { 75 femaleVoice = voices[0]; 76 } 77 78 return femaleVoice; 79 } 80 81 // Função para atualizar o estado dos botões 82 function updateButtonState(state) { 83 if (state === 'playing') { 84 playButton.hide(); 85 pauseButton.show(); 86 stopButton.show(); 87 rateControl.show(); 88 } else if (state === 'paused') { 89 playButton.html('<i class="fas fa-play"></i> ' + leitorDeTextoData.buttonText); // Volta para play 90 playButton.show(); 91 pauseButton.hide(); 92 stopButton.show(); 93 rateControl.show(); 94 } else { // 'stopped' ou 'initial' 95 playButton.html('<i class="fas fa-play"></i> ' + leitorDeTextoData.buttonText); 96 playButton.show(); 97 pauseButton.hide(); 98 stopButton.hide(); 99 rateControl.hide(); 100 } 101 } 102 103 // Função para processar o texto e adicionar à fila de fala 104 function processAndQueueText(rawText) { 105 speechQueue = []; 106 // Divide o texto em sentenças ou parágrafos para melhor controle da leitura 107 var paragraphs = rawText.split(/[\n\r]+/); // Divide por quebras de linha 108 paragraphs.forEach(function(p) { 109 var sentences = p.match(/[^.!?]+[.!?]|\S+/g); // Tenta dividir por sentenças 110 if (sentences) {
111 sentences.forEach(function(s) { 112 s = s.trim(); 113 if (s.length > 0) { 114 speechQueue.push(s); 115 } 116 }); 117 } else if (p.trim().length > 0) { 118 speechQueue.push(p.trim()); 119 } 120 }); 121 } 122 123 // Função para iniciar a próxima parte da fala na fila 124 function speakNextInQueue() { 125 if (speechQueue.length > 0 && !isPaused) { 126 var textToSpeak = speechQueue.shift(); // Pega a próxima parte 127 currentUtterance = new SpeechSynthesisUtterance(textToSpeak); 128 129 if (selectedVoice) { 130 currentUtterance.voice = selectedVoice; 131 currentUtterance.lang = selectedVoice.lang; 132 } else { 133 currentUtterance.lang = leitorDeTextoData.language.replace('_', '-'); 134 } 135 136 currentUtterance.rate = parseFloat(rateInput.val()); 137 currentUtterance.pitch = parseFloat(leitorDeTextoData.defaultPitch); 138 139 currentUtterance.onend = function() { 140 if (speechQueue.length > 0) { 141 speakNextInQueue(); // Continua para a próxima parte 142 } else { 143 // Leitura completa 144 isSpeaking = false; 145 isPaused = false; 146 updateButtonState('stopped'); 147 } 148 }; 149 150 currentUtterance.onerror = function(event) { 151 console.error('Leitor de Texto: Erro na sÃntese de voz para a parte:', event.error, event.utterance.text); 152 isSpeaking = false; 153 isPaused = false; 154 updateButtonState('stopped'); 155 // Não exibir alert aqui para evitar sobrecarga de popups em erros de partes 156 }; 157 158 window.speechSynthesis.speak(currentUtterance); 159 }
159 else if (speechQueue.length === 0 && isSpeaking) { 160 // Leitura completa (se a fila esvaziou e ainda estava falando) 161 isSpeaking = false; 162 isPaused = false; 163 updateButtonState('stopped'); 164 } 165 } 166 167 // Função para calcular e exibir o tempo de leitura 168 function calculateAndDisplayReadingTime(text) { 169 var wordCount = text.split(/\s+/).length; // Conta palavras separadas por espaço 170 var minutes = Math.ceil(wordCount / leitorDeTextoData.wordsPerMinute); 171 172 if (minutes === 0) { 173 readingTimeSpan.text(leitorDeTextoData.lessThanOneMinuteText); 174 } else if (minutes === 1) { 175 readingTimeSpan.text(leitorDeTextoData.readingTimeTextSingular.replace('%d', minutes)); 176 } else { 177 readingTimeSpan.text(leitorDeTextoData.readingTimeTextPlural.replace('%d', minutes)); 178 } 179 } 180 181 // --- Inicialização da Voz --- 182 // Este evento é disparado quando as vozes do sistema estão prontas. 183 window.speechSynthesis.onvoiceschanged = function() { 184 selectedVoice = getBrazilianFemaleVoice(); 185 if (!selectedVoice) { 186 console.warn('Leitor de Texto: Nenhuma voz adequada (feminina pt-BR) encontrada. Usando a voz padrão do navegador ou a primeira disponÃvel.'); 187 } else { 188 console.log('Leitor de Texto: Voz selecionada:', selectedVoice.name, selectedVoice.lang); 189 } 190 }; 191 192 // Força o carregamento das vozes em navegadores onde 'onvoiceschanged' pode não disparar 193 if (window.speechSynthesis.getVoices().length === 0) { 194 window.speechSynthesis.getVoices(); 195 } else { 196 selectedVoice = getBrazilianFemaleVoice(); 197 } 198 199 200 // --- Extrair e Preparar Conteúdo para Leitura e Tempo de Leitura --- 201 function extractContentAndCalculateTime() { 202 var mainContent = $(leitorDeTextoData.contentSelectors); 203 204 if (mainContent.length > 0) { 205 var clonedContent = mainContent.clone(); 206 clonedContent.find('button, script, style, noscript, .screen-reader-text, .wp-block-audio, .wp-block-video, .wp-block-embed, .leitor-de-texto-controls, .gallery, header, footer, nav, aside, .wp-block-site-logo, .wp-block-search, .widget_area, .elementor-element-overlay').remove(); 207 208 // Pega o texto de cada elemento correspondente ao seletor e o junta. 209 // Isso é crucial para seletores como '.entry p, .entry h3' 210 var collectedText = []; 211 clonedContent.each(function() { 212 // Tenta pegar o texto de blocos de parágrafos/cabeçalhos diretamente 213 if ($(this).is('p, h1, h2, h3, h4, h5, h6, em, strong, li')) { 214 collectedText.push($(this).text()); 215 } else { 216 // Para seletores de containers (ex: .entry-content), pega todo o texto de dentro 217 collectedText.push($(this).text()); 218 } 219 }); 220 221 fullContentText = collectedText.join(' ').replace(/\s\s+/g, ' ').trim(); 222 calculateAndDisplayReadingTime(fullContentText); 223 224 } else { 225 console.error('Leitor de Texto: ERRO! Não foi possÃvel encontrar o conteúdo principal para leitura. Verifique os seletores CSS do seu tema no arquivo leitor.php.'); 226 // Não exibe alert aqui para não irritar o usuário no carregamento da página 227 readingTimeSpan.text('Conteúdo não encontrado'); // Indica erro no tempo de leitura 228 playButton.prop('disabled', true); // Desabilita o botão se não houver conteúdo 229 } 230 231 if (fullContentText.length === 0) { 232 console.warn('Leitor de Texto: O conteúdo a ser lido está vazio após a filtragem. Nada para ler.'); 233 readingTimeSpan.text('N/A'); // Indica que não há texto 234 playButton.prop('disabled', true); // Desabilita o botão se o texto estiver vazio 235 } 236 } 237 238 // --- Event Listeners dos Botões --- 239 240 playButton.on('click', function() { 241 if (!selectedVoice) { 242 console.warn('Leitor de Texto: Nenhuma voz disponÃvel para leitura. Verifique as configurações de voz do seu navegador/sistema operacional.'); 243 alert('Não foi possÃvel iniciar a leitura. Por favor, verifique se seu navegador possui vozes de leitura instaladas e ativadas.'); 244 return; 245 } 246 247 if (isPaused) { 248 window.speechSynthesis.resume(); 249 isPaused = false;
250 isSpeaking = true; 251 updateButtonState('playing'); 252 return; 253 } 254 255 if (fullContentText.length === 0) { 256 alert('Não há conteúdo textual para ser lido nesta página.'); 257 return; 258 } 259 260 // Limpa qualquer leitura anterior antes de iniciar uma nova 261 window.speechSynthesis.cancel(); 262 isSpeaking = true; 263 isPaused = false; 264 updateButtonState('playing'); 265 266 processAndQueueText(fullContentText); // Processa o texto completo 267 speakNextInQueue(); 268 }); 269 270 pauseButton.on('click', function() { 271 if (isSpeaking && !isPaused) { 272 window.speechSynthesis.pause(); 273 isPaused = true; 274 updateButtonState('paused'); 275 } 276 }); 277 278 stopButton.on('click', function() { 279 if (isSpeaking || isPaused) { 280 window.speechSynthesis.cancel(); // Cancela a leitura 281 isSpeaking = false; 282 isPaused = false; 283 speechQueue = []; // Limpa a fila de fala 284 updateButtonState('stopped'); 285 } 286 }); 287 288 // Adiciona um listener para quando a janela for fechada ou recarregada 289 $(window).on('beforeunload', function() { 290 if (isSpeaking || isPaused) { 291 window.speechSynthesis.cancel(); 292 } 293 }); 294 295 // Garante que os botões tenham o estado inicial correto e calcula o tempo de leitura na carga da página 296 updateButtonState('stopped'); 297 extractContentAndCalculateTime(); // Chama esta função para calcular o tempo ao carregar a página 298});
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.