// BB4S Text Reader v2 - Unified controls, volume, always visible (function() { 'use strict'; let currentParagraphs = []; let currentParagraphIndex = 0; let currentMatchIndex = 0; let highlightsInSelection = 0; // Count of search highlights in current reading selection let totalMatches = 0; let isReading = false; let isPaused = false; let autoPause = true; // Default ON let skipBrackets = true; // Default ON - skip text in square brackets let utterance = null; let volume = 0.8; let speechRate = 0.85; // New: speech rate control const TRANSLATE_API = 'https://api.mymemory.translated.net/get'; // Detect language and word limit from URL const urlParams = new URLSearchParams(window.location.search); const language = urlParams.get('lang') === 'sl' ? 'sl' : 'en'; const WORD_LIMIT = parseInt(urlParams.get('limit')) || 222; console.log('BB4S Text Reader v2 - Language:', language, '| Word Limit:', WORD_LIMIT); function init() { createReaderUI(); attachKeyboardListeners(); updateMatchCounter(); translateUIIfSlovenian(); } function translateUIIfSlovenian() { if (language !== 'sl') return; // Update initial status text document.getElementById('reader-status').textContent = 'Pritisnite B za branje odstavka'; // Translate checkbox labels const autoPauseLabel = document.querySelector('label[for="reader-auto-pause"]'); if (!autoPauseLabel) { // If no explicit label with 'for', find the label containing the checkbox const autoPauseCheckbox = document.getElementById('reader-auto-pause'); if (autoPauseCheckbox && autoPauseCheckbox.parentElement.tagName === 'LABEL') { autoPauseCheckbox.parentElement.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('Auto-pause')) { node.textContent = ' Samodejni premor'; } }); } } const skipBracketsLabel = document.querySelector('label[for="reader-skip-brackets"]'); if (!skipBracketsLabel) { const skipBracketsCheckbox = document.getElementById('reader-skip-brackets'); if (skipBracketsCheckbox && skipBracketsCheckbox.parentElement.tagName === 'LABEL') { skipBracketsCheckbox.parentElement.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('Skip []')) { node.textContent = ' Presko\u010Di []'; } }); } } } function createReaderUI() { if (document.getElementById('unified-reader-controls')) return; const initialText = language === 'sl' ? 'Pritisnite R za branje odstavka' : 'Press R to read paragraph'; const controls = document.createElement('div'); controls.id = 'unified-reader-controls'; controls.innerHTML = `
tags let node = element; while (node && node !== document.body) { if (node.tagName === 'P') { const text = getCleanText(node); if (text.length > 2) return node; } node = node.parentElement; } // Second pass: fallback to DIV, TD, LI if no
found node = element; while (node && node !== document.body) { const tagName = node.tagName; if (tagName === 'DIV' || tagName === 'TD' || tagName === 'LI') { const text = getCleanText(node); if (text.length > 2) return node; } node = node.parentElement; } return null; } function collectParagraphs(startParagraph) { const paragraphs = []; let totalWords = 0; let currentNode = startParagraph; console.log('Collecting paragraphs with WORD_LIMIT:', WORD_LIMIT); while (currentNode && totalWords < WORD_LIMIT) { const text = getCleanText(currentNode); const words = text.split(/\s+/).filter(w => w.length > 0).length; // Check if adding this paragraph would exceed limit if (totalWords + words > WORD_LIMIT) { // Exception: if it's the first paragraph, take it anyway but truncate if (paragraphs.length === 0 && words > WORD_LIMIT) { console.log('First para exceeds limit (', words, '>', WORD_LIMIT, ') - truncating'); const truncatedWords = text.split(/\s+/).slice(0, WORD_LIMIT).join(' '); paragraphs.push({ element: currentNode, text: truncatedWords, words: WORD_LIMIT, truncated: true, originalWords: words }); totalWords = WORD_LIMIT; break; } console.log('Stopping: next para would exceed limit', totalWords, '+', words, '>', WORD_LIMIT); break; } if (words > 0) { paragraphs.push({ element: currentNode, text: text, words: words }); totalWords += words; console.log('Added para:', words, 'words | Total so far:', totalWords); } currentNode = getNextParagraph(currentNode); } console.log('Final collection:', paragraphs.length, 'paragraphs,', totalWords, 'words'); return paragraphs; } function getCleanText(element) { const clone = element.cloneNode(true); clone.querySelectorAll('script, style, img, svg').forEach(el => el.remove()); return clone.textContent.trim().replace(/\s+/g, ' '); } function getNextParagraph(element) { let next = element.nextElementSibling; while (next) { const tagName = next.tagName; if (tagName === 'P' || tagName === 'DIV' || tagName === 'TD' || tagName === 'LI') { const text = getCleanText(next); if (text.length > 2) return next; } next = next.nextElementSibling; } return null; } async function readCurrentParagraph() { if (currentParagraphIndex >= currentParagraphs.length) { stopReading(); return; } const para = currentParagraphs[currentParagraphIndex]; let textToRead = para.text; // Remove text in square brackets if skipBrackets is enabled if (skipBrackets) { textToRead = textToRead.replace(/\[[^\]]*\]/g, ''); // Also remove URLs (anything starting with www. or http:// or https://) textToRead = textToRead.replace(/\b(https?:\/\/[^\s]+|www\.[^\s]+)/gi, ''); } highlightCurrentParagraph(); const totalWords = currentParagraphs.reduce((sum, p) => sum + p.words, 0); const matchInfo = currentMatchIndex > 0 ? ` around match ${currentMatchIndex}` : ''; if (language === 'sl') { const matchInfoSl = currentMatchIndex > 0 ? ` okoli ujemanja ${currentMatchIndex}` : ''; updateStatus(`Prebrano ${totalWords} besed${matchInfoSl}`); } else { updateStatus(`Read ${totalWords} words${matchInfo}`); } // Translate if Slovenian if (language === 'sl') { updateStatus('Prevajanje v slovenščino...'); try { textToRead = await translateText(textToRead); const matchInfoSl = currentMatchIndex > 0 ? ` okoli ujemanja ${currentMatchIndex}` : ''; updateStatus(`Prebrano ${totalWords} besed${matchInfoSl} - Slovensko`); } catch (error) { console.error('Translation failed:', error); updateStatus(`Prevajanje ni uspelo - branje v angleščini`); // Continue with English } } speak(textToRead); } async function translateText(text) { const truncated = text.substring(0, 500); // MyMemory free tier limit const url = `${TRANSLATE_API}?q=${encodeURIComponent(truncated)}&langpair=en|sl`; const response = await fetch(url); if (!response.ok) { throw new Error(`API returned ${response.status}`); } const data = await response.json(); if (data.responseData && data.responseData.translatedText) { return data.responseData.translatedText; } throw new Error('No translated text in response'); } function speak(text) { utterance = new SpeechSynthesisUtterance(text); utterance.rate = speechRate; utterance.pitch = 1.0; utterance.volume = volume; utterance.lang = language === 'sl' ? 'sl-SI' : 'en-US'; const voices = window.speechSynthesis.getVoices(); let selectedVoice = null; if (language === 'sl') { selectedVoice = voices.find(v => v.lang.includes('sl')); if (selectedVoice) { console.log('Using Slovenian voice:', selectedVoice.name); } else { console.log('No Slovenian voice available - will sound accented'); } } else { selectedVoice = voices.find(v => v.lang.includes('en') && v.name.toLowerCase().includes('female') ); } if (selectedVoice) { utterance.voice = selectedVoice; } utterance.onend = function() { if (autoPause) { isPaused = true; updatePlayPauseButton(); updateStatus('Paused (auto-pause)'); setTimeout(() => { if (isPaused && currentParagraphIndex < currentParagraphs.length - 1) { currentParagraphIndex++; isPaused = false; readCurrentParagraph(); } else if (currentParagraphIndex >= currentParagraphs.length - 1) { // Reading completed - advance counter if multiple highlights if (highlightsInSelection > 1) { advanceMatchCounter(highlightsInSelection - 1); } stopReading(); } }, 2000); } else { currentParagraphIndex++; if (currentParagraphIndex < currentParagraphs.length) { readCurrentParagraph(); } else { // Reading completed - advance counter if multiple highlights if (highlightsInSelection > 1) { advanceMatchCounter(highlightsInSelection - 1); } stopReading(); } } }; window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); updatePlayPauseButton(); } function togglePlayPause() { if (isPaused) { isPaused = false; if (currentParagraphIndex < currentParagraphs.length) { readCurrentParagraph(); } } else if (isReading) { isPaused = true; window.speechSynthesis.cancel(); updateStatus('Paused'); } updatePlayPauseButton(); } function stopReading() { isReading = false; isPaused = false; window.speechSynthesis.cancel(); clearHighlights(); disableControls(); const resetText = language === 'sl' ? 'Pritisnite R za branje odstavka' : 'Press R to read paragraph'; updateStatus(resetText); // Report total words read to hex page (if opened from hex page) if (window.opener && currentParagraphs.length > 0) { const totalWords = currentParagraphs.reduce((sum, p) => sum + p.words, 0); const hexCode = extractHexCode(); if (hexCode) { try { window.opener.postMessage({ type: 'wordCount', hexCode: hexCode, words: totalWords }, '*'); console.log(`Reported ${totalWords} words to hex page for ${hexCode}`); } catch(e) { console.log('Could not report to hex page:', e); } } } } // Extract hex code from current URL (e.g., xy.html -> xy) function extractHexCode() { const path = window.location.pathname; const match = path.match(/\/([a-z]{2})\.html$/i); return match ? match[1].toLowerCase() : null; } function highlightCurrentParagraph() { document.querySelectorAll('.reading-paragraph').forEach(el => { el.classList.remove('reading-paragraph'); }); if (currentParagraphs[currentParagraphIndex]) { const para = currentParagraphs[currentParagraphIndex]; const elem = para.element; console.log(`📘 Highlighting paragraph ${currentParagraphIndex + 1}/${currentParagraphs.length}: <${elem.tagName}> with ${para.words} words`); elem.classList.add('reading-paragraph'); elem.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Show truncation notice in status if truncated if (para.truncated) { console.log(`⚠️ Paragraph truncated: reading ${para.words} of ${para.originalWords} words`); } } } function clearHighlights() { document.querySelectorAll('.reading-paragraph').forEach(el => { el.classList.remove('reading-paragraph'); }); } function updateStatus(text) { document.getElementById('reader-status').textContent = text; } function updatePlayPauseButton() { const btn = document.getElementById('reader-play-pause'); if (isReading && !isPaused) { btn.textContent = language === 'sl' ? 'Premor' : 'Pause'; btn.classList.add('playing'); } else { btn.textContent = language === 'sl' ? 'Predvajaj' : 'Play'; btn.classList.remove('playing'); } } function enableControls() { document.getElementById('reader-play-pause').disabled = false; document.getElementById('reader-stop').disabled = false; } function disableControls() { document.getElementById('reader-play-pause').disabled = true; document.getElementById('reader-stop').disabled = true; } window.addEventListener('load', function() { setTimeout(init, 100); }); })();