// ====== Text-to-Speech for Blogger (Language Picker, External JS) ====== // Self-contained: injects its own controls & styles above each post (.post-body) (function(){ const STORAGE_KEY_LANG = 'crml_tts_lang_v1'; // remember language choice // ---- Inject minimal CSS once ---- (function injectCSS(){ if (document.getElementById('crml-tts-style')) return; const css = ` .crml-actions{background:#fff0c2;padding:12px 14px;border-radius:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:12px 0;box-shadow:0 2px 6px rgba(0,0,0,.06)} .crml-label{font-size:13px;opacity:.85} .crml-select{appearance:none;padding:8px 12px;border-radius:12px;border:1px solid #d7dde3;background:#fff;font-size:14px} .crml-btn{border:0;border-radius:16px;padding:10px 16px;font-weight:600;font-size:14px;cursor:pointer;display:inline-flex;gap:8px;align-items:center;box-shadow:0 2px 4px rgba(0,0,0,.06);background:#2787f5;color:#fff} .crml-btn:disabled{opacity:.5;cursor:not-allowed} .crml-btn:active{transform:translateY(1px)} .crml-btn-stop{background:#FF6C00 !important;color:#fff !important} `; const style = document.createElement('style'); style.id = 'crml-tts-style'; style.textContent = css; document.head.appendChild(style); })(); const TTS = { utter: null, state: 'idle', // idle | playing | paused voices: [], byLang: new Map(), loadVoices(){ this.voices = window.speechSynthesis.getVoices() || []; this.byLang.clear(); this.voices.forEach(v=>{ const L = (v.lang || '').trim(); if (!this.byLang.has(L)) this.byLang.set(L, []); this.byLang.get(L).push(v); }); }, pickVoice(langHint){ const vs = this.voices.length ? this.voices : window.speechSynthesis.getVoices(); const lang = (langHint||'').toLowerCase(); // exact match (e.g., el-GR) let v = vs.find(v => (v.lang||'').toLowerCase() === lang); if (v) return v; // base language match (e.g., el-*) const base = lang.split('-')[0]; v = vs.find(v => (v.lang||'').toLowerCase().startsWith(base)); if (v) return v; // heuristics for Greek / English if (base === 'el'){ v = vs.find(v => /greek/i.test(v.name||'')) || vs.find(v => (v.lang||'').toLowerCase().startsWith('el')); if (v) return v; } if (base === 'en'){ v = vs.find(v => /^en-(gb|us)/i.test(v.lang||'')) || vs.find(v => (v.lang||'').toLowerCase().startsWith('en')); if (v) return v; } // fallback return vs[0] || null; }, textFrom(root){ const body = root.querySelector('.post-body,[itemprop="articleBody"]') || document.querySelector('.post-body,[itemprop="articleBody"]'); const c = (body ? body.cloneNode(true) : null); if (!c) return ''; c.querySelectorAll('script,style,noscript,iframe,svg,canvas,.MathJax,mjx-container').forEach(n=>n.remove()); let text = (c.innerText || '').replace(/\s+\n/g,'\n').replace(/\n{3,}/g,'\n\n').trim(); if (text.length>100000) text = text.slice(0,100000); return text; }, getChosenLang(selectEl){ const v = (selectEl && selectEl.value) ? selectEl.value : '__auto__'; if (v && v !== '__auto__') return v; const htmlLang = (document.documentElement.lang || '').trim(); const navLang = (navigator.language || 'en-US'); const langs = Array.from(this.byLang.keys()); const pick = want => langs.find(L => L.toLowerCase().startsWith(want.toLowerCase().split('-')[0])); return pick(htmlLang) || pick(navLang) || 'en-US'; }, start(btn){ if (!('speechSynthesis' in window)) { alert('Ο browser δεν υποστηρίζει Text-to-Speech.'); return; } const row = btn.closest('.crml-actions') || btn.parentElement || document; const langSelect = row.querySelector('[data-tts="lang"]'); const root = btn.closest('.post') || document; const text = this.textFrom(root); if (!text) { alert('Δεν βρέθηκε κείμενο για ανάγνωση.'); return; } window.speechSynthesis.cancel(); this.loadVoices(); const chosenLang = this.getChosenLang(langSelect); const u = new SpeechSynthesisUtterance(text); u.lang = chosenLang; u.rate = 1; u.pitch = 1; const voice = this.pickVoice(chosenLang); if (voice) u.voice = voice; u.onstart = ()=>{ this.state='playing'; this.updateButtons(btn,'playing'); }; u.onend = ()=>{ this.state='idle'; this.updateButtons(btn,'idle'); }; u.onerror = ()=>{ this.state='idle'; this.updateButtons(btn,'idle'); }; try { localStorage.setItem(STORAGE_KEY_LANG, langSelect ? (langSelect.value || '__auto__') : '__auto__'); } catch(e){} this.utter = u; window.speechSynthesis.speak(u); }, toggle(btn){ if (!('speechSynthesis' in window)) { alert('Ο browser δεν υποστηρίζει Text-to-Speech.'); return; } if (this.state==='playing'){ window.speechSynthesis.pause(); this.state='paused'; this.updateButtons(btn,'paused'); } else if (this.state==='paused'){ window.speechSynthesis.resume(); this.state='playing'; this.updateButtons(btn,'playing'); } else { this.start(btn); } }, stop(btn){ window.speechSynthesis.cancel(); this.state='idle'; this.updateButtons(btn,'idle'); }, updateButtons(anyBtn,state){ const row = anyBtn.closest('.crml-actions') || anyBtn.parentElement; const playBtn = row ? row.querySelector('[data-tts="toggle"]') : null; const stopBtn = row ? row.querySelector('[data-tts="stop"]') : null; if (!playBtn || !stopBtn) return; if (state==='playing'){ playBtn.textContent = '⏸️ Παύση'; stopBtn.removeAttribute('disabled'); } else if (state==='paused'){ playBtn.textContent = '▶️ Συνέχεια'; stopBtn.removeAttribute('disabled'); } else { playBtn.textContent = '🔊 Listen'; stopBtn.setAttribute('disabled','disabled'); } } }; window.CRML_TTS = TTS; if ('speechSynthesis' in window){ window.speechSynthesis.onvoiceschanged = function(){ window.CRML_TTS.loadVoices(); }; } function buildLangOptions(){ const saved = (function(){ try { return localStorage.getItem(STORAGE_KEY_LANG) || '__auto__'; } catch(e){ return '__auto__'; } })(); const sel = document.createElement('select'); sel.setAttribute('data-tts','lang'); sel.className = 'crml-select'; sel.innerHTML = ''; TTS.loadVoices(); const langs = Array.from(TTS.byLang.keys()).sort((a,b)=>a.localeCompare(b)); langs.forEach(L=>{ const opt = document.createElement('option'); opt.value = L; opt.textContent = L; sel.appendChild(opt); }); sel.value = saved; sel.title = 'Επιλογή γλώσσας αναπαραγωγής'; return sel; } function makeRow(){ const row = document.createElement('div'); row.className = 'crml-actions'; const langLabel = document.createElement('span'); langLabel.textContent = 'Language: '; langLabel.className = 'crml-label'; const sel = buildLangOptions(); const btnPlay = document.createElement('button'); btnPlay.className = 'crml-btn'; btnPlay.setAttribute('data-tts','toggle'); btnPlay.textContent = '🔊 Listen'; btnPlay.onclick = function(){ CRML_TTS.toggle(this); }; const btnStop = document.createElement('button'); btnStop.className = 'crml-btn crml-btn-stop'; btnStop.setAttribute('data-tts','stop'); btnStop.setAttribute('disabled','disabled'); btnStop.textContent = '■ Stop'; btnStop.onclick = function(){ CRML_TTS.stop(this); }; row.appendChild(langLabel); row.appendChild(sel); row.appendChild(btnPlay); row.appendChild(btnStop); return row; } function injectOnce(postRoot){ if (!postRoot) return; if (postRoot.querySelector('.crml-actions')) return; const body = postRoot.querySelector('.post-body,[itemprop="articleBody"]') || postRoot; const row = makeRow(); if (body && body.parentElement) { body.parentElement.insertBefore(row, body); } } function findPostRoots(){ const roots = new Set(); document.querySelectorAll('.post, article, .post-outer, [itemtype*="Article"], .blog-posts') .forEach(node=>{ const root = node.closest('.post, article, .post-outer') || node; if (root) roots.add(root); }); if (roots.size===0) { document.querySelectorAll('.post-body,[itemprop="articleBody"]').forEach(b=>{ roots.add(b.parentElement || b); }); } return Array.from(roots); } function init(){ const posts = findPostRoots(); posts.forEach(injectOnce); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } new MutationObserver(()=>init()).observe(document.documentElement, {childList:true, subtree:true}); })();