// 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 = `
${initialText}
`; document.body.appendChild(controls); addReaderStyles(); attachControlListeners(); } function addReaderStyles() { if (document.getElementById('text-reader-styles-v2')) return; const style = document.createElement('style'); style.id = 'text-reader-styles-v2'; style.textContent = ` #unified-reader-controls { position: fixed; bottom: 20px; left: 200px; right: 225px; background: linear-gradient(135deg, #1e5a8e, #2670ad); color: #d4af37; padding: 8px 15px; font-family: 'Courier New', monospace; font-size: 16px; font-weight: bold; z-index: 10001; display: flex; justify-content: space-between; align-items: center; gap: 15px; border: 1px solid #d4af37; clip-path: polygon( 10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px), 0 10px ); } .reader-left { flex: 0 0 auto; display: flex; gap: 12px; align-items: center; } .reader-center { flex: 1; text-align: center; } .reader-right { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; } .volume-control, .speed-control { display: flex; align-items: center; gap: 6px; color: #ffeb3b; font-size: 14px; } #reader-volume { width: 80px; accent-color: #c62828; } #reader-speed { width: 80px; accent-color: #76ff03; } #unified-reader-controls button { background: linear-gradient(135deg, #cd7f32, #d4af37, #b87333); border: 1px solid #d4af37; padding: 4px 10px; border-radius: 3px; font-weight: bold; cursor: pointer; font-family: 'Courier New', monospace; font-size: 13px; color: #1a1410; } #unified-reader-controls button:hover:not(:disabled) { background: linear-gradient(135deg, #b87333, #cd7f32, #d4af37); } #unified-reader-controls button:disabled { background: #555; border-color: #666; cursor: not-allowed; opacity: 0.5; } #reader-play-pause.playing { background: linear-gradient(135deg, #ff9800, #ffb74d); } #reader-status { font-weight: bold; margin-right: 12px; color: #d4af37; font-size: 16px; } #reader-match-info { color: #ffeb3b; font-size: 14px; } #unified-reader-controls label { font-size: 13px; display: flex; align-items: center; gap: 4px; } .reading-paragraph { background-color: rgba(33, 150, 243, 0.1) !important; border-left: 4px solid #2196f3 !important; padding-left: 12px !important; transition: background-color 0.3s; } /* Hide old search-highlighter counter */ #search-match-counter { display: none !important; } `; document.head.appendChild(style); } function attachControlListeners() { document.getElementById('reader-play-pause').addEventListener('click', togglePlayPause); document.getElementById('reader-prev').addEventListener('click', previousMatch); document.getElementById('reader-next').addEventListener('click', nextMatch); document.getElementById('reader-stop').addEventListener('click', stopReading); document.getElementById('reader-auto-pause').addEventListener('change', function() { autoPause = this.checked; }); document.getElementById('reader-skip-brackets').addEventListener('change', function() { skipBrackets = this.checked; }); document.getElementById('reader-volume').addEventListener('input', function() { volume = this.value / 100; // If currently reading, restart paragraph with new volume if (isReading && !isPaused) { const currentIdx = currentParagraphIndex; window.speechSynthesis.cancel(); currentParagraphIndex = currentIdx; setTimeout(() => readCurrentParagraph(), 100); } }); document.getElementById('reader-speed').addEventListener('input', function() { speechRate = this.value / 100; // If currently reading, restart paragraph with new speed if (isReading && !isPaused) { const currentIdx = currentParagraphIndex; window.speechSynthesis.cancel(); currentParagraphIndex = currentIdx; setTimeout(() => readCurrentParagraph(), 100); } }); } function attachKeyboardListeners() { document.addEventListener('keydown', function(e) { // R key (English) or B key (Slovenian): Read paragraph at current match const readKey = language === 'sl' ? 'b' : 'r'; console.log(`Key pressed: "${e.key}" | Expected: "${readKey}" | Language: ${language}`); if ((e.key === readKey || e.key === readKey.toUpperCase()) && !isReading) { const currentMatch = document.querySelector('mark.search-highlight.current'); if (currentMatch) { readParagraphFromElement(currentMatch); } else { // Try first match const firstMatch = document.querySelector('mark.search-highlight'); if (firstMatch) { firstMatch.classList.add('current'); readParagraphFromElement(firstMatch); } } e.preventDefault(); } // Spacebar: Play/Pause if (e.code === 'Space' && isReading) { togglePlayPause(); e.preventDefault(); } }); } function updateMatchCounter() { const matches = document.querySelectorAll('mark.search-highlight'); totalMatches = matches.length; if (totalMatches > 0) { const current = document.querySelector('mark.search-highlight.current'); if (current) { currentMatchIndex = Array.from(matches).indexOf(current) + 1; } const matchText = language === 'sl' ? 'Ujemanje' : 'Match'; document.getElementById('reader-match-info').textContent = `${matchText} ${currentMatchIndex}/${totalMatches}`; } else { document.getElementById('reader-match-info').textContent = ''; } } function advanceMatchCounter(count) { // Advance the match counter by moving to the next N matches const matches = Array.from(document.querySelectorAll('mark.search-highlight')); if (matches.length === 0) return; const current = document.querySelector('mark.search-highlight.current'); if (!current) return; let currentIdx = matches.indexOf(current); let targetIdx = (currentIdx + count) % matches.length; current.classList.remove('current'); matches[targetIdx].classList.add('current'); console.log(`📈 Advanced counter by ${count} (from match ${currentIdx + 1} to ${targetIdx + 1})`); updateMatchCounter(); } function previousMatch() { const matches = Array.from(document.querySelectorAll('mark.search-highlight')); if (matches.length === 0) return; const current = document.querySelector('mark.search-highlight.current'); let prevIndex = 0; if (current) { const currentIdx = matches.indexOf(current); prevIndex = currentIdx > 0 ? currentIdx - 1 : matches.length - 1; current.classList.remove('current'); } else { // No current match, start from end prevIndex = matches.length - 1; } matches[prevIndex].classList.add('current'); matches[prevIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); updateMatchCounter(); // Always start reading when prev button clicked if (isReading) { stopReading(); } setTimeout(() => readParagraphFromElement(matches[prevIndex]), 100); } function nextMatch() { const matches = Array.from(document.querySelectorAll('mark.search-highlight')); if (matches.length === 0) return; const current = document.querySelector('mark.search-highlight.current'); let nextIndex = 0; if (current) { const currentIdx = matches.indexOf(current); nextIndex = currentIdx < matches.length - 1 ? currentIdx + 1 : 0; current.classList.remove('current'); } else { // No current match, start from beginning nextIndex = 0; } matches[nextIndex].classList.add('current'); matches[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); updateMatchCounter(); // Always start reading when next button clicked if (isReading) { stopReading(); } setTimeout(() => readParagraphFromElement(matches[nextIndex]), 100); } function readParagraphFromElement(element) { const paragraph = findParagraphContainer(element); if (!paragraph) return; const paragraphs = collectParagraphs(paragraph); if (paragraphs.length === 0) return; currentParagraphs = paragraphs; currentParagraphIndex = 0; isReading = true; isPaused = false; // Count search highlights in all collected paragraphs highlightsInSelection = 0; paragraphs.forEach(p => { if (p.element) { const highlights = p.element.querySelectorAll('mark.search-highlight'); highlightsInSelection += highlights.length; } }); console.log(`📊 Found ${highlightsInSelection} search highlights in ${paragraphs.length} paragraphs`); enableControls(); readCurrentParagraph(); } function findParagraphContainer(element) { // First pass: look for actual

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); }); })();