451 lines
21 KiB
HTML
451 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Shadow Reader — Practice Speaking English</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🗣️</text></svg>">
|
|
<style>
|
|
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--bg:var(--ink);--surface:#1A1F2E;--text:var(--white);--text-muted:#9aa3b2;--border:#2A3040;--accent:var(--cyan);--accent-hover:#00c8e0;--selection-bg:rgba(0,229,255,.18);--reader-bg:#141824}
|
|
[data-theme=light]{--bg:#f5f6fa;--surface:#fff;--text:var(--ink);--text-muted:var(--gray-500);--border:#e1e4e8;--reader-bg:#fff}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--text);background:var(--bg);min-height:100vh;transition:background .3s,color .3s}
|
|
|
|
/* === WELCOME SCREEN === */
|
|
.welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:40px 24px;text-align:center}
|
|
.welcome .logo{font-size:64px;margin-bottom:16px}
|
|
.welcome h1{font-size:42px;font-weight:800;line-height:1.1;margin-bottom:12px;background:linear-gradient(135deg,var(--cyan),#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
|
.welcome p{color:var(--text-muted);max-width:480px;margin-bottom:32px;font-size:18px}
|
|
.dropzone{width:100%;max-width:520px;border:2px dashed var(--border);border-radius:16px;padding:48px 32px;cursor:pointer;transition:border-color .2s,background .2s;display:flex;flex-direction:column;align-items:center;gap:12px}
|
|
.dropzone:hover,.dropzone.dragover{border-color:var(--accent);background:var(--selection-bg)}
|
|
.dropzone .icon{font-size:48px}
|
|
.dropzone span{color:var(--text-muted);font-size:15px}
|
|
.dropzone strong{color:var(--text)}
|
|
#fileInput{display:none}
|
|
.sample-btn{margin-top:16px;background:none;border:1px solid var(--border);color:var(--text-muted);padding:10px 20px;border-radius:8px;cursor:pointer;font-size:14px;transition:all .2s}
|
|
.sample-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
|
|
/* === APP LAYOUT === */
|
|
.app{display:none;min-height:100vh;flex-direction:column}
|
|
.app.active{display:flex}
|
|
|
|
/* HEADER */
|
|
.header{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:100;gap:12px}
|
|
.header-left{display:flex;align-items:center;gap:12px}
|
|
.header .logo-sm{font-size:24px}
|
|
.header h1{font-size:18px;font-weight:700}
|
|
.header-right{display:flex;align-items:center;gap:8px}
|
|
.icon-btn{background:none;border:1px solid var(--border);color:var(--text-muted);width:38px;height:38px;border-radius:8px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;transition:all .2s;position:relative}
|
|
.icon-btn:hover{border-color:var(--accent);color:var(--accent)}
|
|
|
|
/* READER */
|
|
.reader{flex:1;overflow-y:auto;padding:40px 24px;max-width:800px;margin:0 auto;width:100%;background:var(--reader-bg)}
|
|
.reader-content{font-size:19px;line-height:1.85;color:var(--text)}
|
|
.reader-content p{margin-bottom:1.4em;text-align:justify}
|
|
.reader-content ::selection{background:var(--selection-bg);color:var(--cyan)}
|
|
|
|
/* FLOATING PLAY BUTTON */
|
|
.floating-play{position:fixed;z-index:999;display:flex;align-items:center;gap:8px;background:var(--accent);color:var(--ink);padding:10px 20px;border-radius:30px;font-weight:700;font-size:15px;cursor:pointer;box-shadow:0 8px 32px rgba(0,229,255,.35);transform:translate(-50%,-120%);opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;user-select:none}
|
|
.floating-play.visible{opacity:1;pointer-events:auto;transform:translate(-50%,-120%)}
|
|
.floating-play .play-icon{font-size:18px}
|
|
.floating-play:hover{transform:translate(-50%,-120%) scale(1.05)}
|
|
.floating-play:active{transform:translate(-50%,-120%) scale(.97)}
|
|
|
|
/* PLAYBACK BAR */
|
|
.playback-bar{position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-top:1px solid var(--border);padding:12px 20px;display:none;align-items:center;gap:16px;z-index:200;flex-wrap:wrap}
|
|
.playback-bar.active{display:flex}
|
|
.playback-bar .track-info{flex:1;min-width:200px;font-size:14px;color:var(--text-muted);overflow:hidden;text-ellipsis;white-space:nowrap}
|
|
.playback-bar .controls{display:flex;align-items:center;gap:8px}
|
|
.playback-bar .ctrl-btn{width:42px;height:42px;border-radius:50%;border:none;background:var(--accent);color:var(--ink);font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform .15s}
|
|
.playback-bar .ctrl-btn:hover{transform:scale(1.1)}
|
|
.playback-bar .ctrl-btn.secondary{background:var(--border);color:var(--text-muted);width:36px;height:36px;font-size:14px}
|
|
.playback-bar .progress-track{flex:1;min-width:120px;height:4px;background:var(--border);border-radius:2px;cursor:pointer;position:relative}
|
|
.playback-bar .progress-fill{height:100%;background:var(--accent);border-radius:2px;width:0%;transition:width .1s linear}
|
|
|
|
/* SETTINGS PANEL */
|
|
.settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:300;display:none;align-items:flex-end;justify-content:center}
|
|
.settings-overlay.open{display:flex}
|
|
.settings-panel{background:var(--surface);border-radius:20px 20px 0 0;padding:28px 24px 40px;width:100%;max-width:500px;max-height:80vh;overflow-y:auto;transform:translateY(100%);transition:transform .3s}
|
|
.settings-overlay.open .settings-panel{transform:translateY(0)}
|
|
.settings-panel h2{font-size:22px;font-weight:700;margin-bottom:24px}
|
|
.setting-group{margin-bottom:24px}
|
|
.setting-group label{display:block;font-size:13px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
|
|
.setting-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
select,input[type=range]{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:10px 14px;border-radius:10px;font-size:15px;cursor:pointer;outline:none}
|
|
select:focus,input[type=range]:focus{border-color:var(--accent)}
|
|
select{min-width:160px}
|
|
input[type=range]{-webkit-appearance:none;appearance:none;height:6px;border-radius:3px;outline:none;padding:0}
|
|
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:var(--accent);cursor:pointer}
|
|
.speed-value{font-size:14px;color:var(--accent);font-weight:700;min-width:44px;text-align:center}
|
|
.theme-toggle{display:flex;gap:8px}
|
|
.theme-option{padding:8px 16px;border-radius:8px;border:1px solid var(--border);background:none;color:var(--text-muted);cursor:pointer;font-size:14px;transition:all .2s}
|
|
.theme-option.active{border-color:var(--accent);color:var(--accent);background:var(--selection-bg)}
|
|
.close-settings{position:absolute;top:16px;right:16px;background:none;border:none;color:var(--text-muted);font-size:24px;cursor:pointer}
|
|
|
|
/* PROGRESS INDICATOR */
|
|
.read-progress{position:fixed;top:60px;left:0;right:0;height:3px;background:var(--border);z-index:99}
|
|
.read-progress-bar{height:100%;background:var(--accent);width:0%;transition:width .2s}
|
|
|
|
/* TOAST */
|
|
.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--surface);color:var(--text);padding:12px 24px;border-radius:12px;border:1px solid var(--border);font-size:14px;opacity:0;pointer-events:none;transition:all .3s;z-index:500}
|
|
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
|
|
/* RESPONSIVE */
|
|
@media(max-width:640px){
|
|
.welcome h1{font-size:32px}
|
|
.welcome p{font-size:16px}
|
|
.dropzone{padding:32px 20px}
|
|
.reader{padding:24px 16px}
|
|
.reader-content{font-size:17px;line-height:1.75}
|
|
.header{padding:10px 14px}
|
|
.header h1{font-size:15px}
|
|
.playback-bar{padding:10px 14px;gap:10px}
|
|
.floating-play{padding:8px 16px;font-size:14px}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- WELCOME / IMPORT -->
|
|
<div class="welcome" id="welcome">
|
|
<div class="logo">🗣️</div>
|
|
<h1>Shadow Reader</h1>
|
|
<p>Import any text, select a passage, and practice speaking by shadowing natural speech synthesis.</p>
|
|
<div class="dropzone" id="dropzone">
|
|
<div class="icon">📄</div>
|
|
<span><strong>Click to upload</strong> or drag & drop a TXT file</span>
|
|
<input type="file" id="fileInput" accept=".txt">
|
|
</div>
|
|
<button class="sample-btn" id="sampleBtn">Try with sample text</button>
|
|
</div>
|
|
|
|
<!-- APP -->
|
|
<div class="app" id="app">
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<span class="logo-sm">🗣️</span>
|
|
<h1>Shadow Reader</h1>
|
|
</div>
|
|
<div class="header-right">
|
|
<button class="icon-btn" id="themeToggle" title="Theme">🌓</button>
|
|
<button class="icon-btn" id="settingsBtn" title="Settings">⚙️</button>
|
|
<button class="icon-btn" id="importBtn" title="Import new">📂</button>
|
|
</div>
|
|
</header>
|
|
<div class="read-progress"><div class="read-progress-bar" id="progressBar"></div></div>
|
|
<main class="reader" id="reader">
|
|
<div class="reader-content" id="readerContent"></div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- FLOATING PLAY -->
|
|
<div class="floating-play" id="floatingPlay"><span class="play-icon">▶️</span> Play selected</div>
|
|
|
|
<!-- PLAYBACK BAR -->
|
|
<div class="playback-bar" id="playbackBar">
|
|
<div class="track-info" id="trackInfo">Select text to play</div>
|
|
<div class="controls">
|
|
<button class="ctrl-btn secondary" id="repeatBtn" title="Repeat loop">🔁</button>
|
|
<button class="ctrl-btn" id="playPauseBtn" title="Play/Pause">▶️</button>
|
|
<button class="ctrl-btn secondary" id="stopBtn" title="Stop">⏹️</button>
|
|
</div>
|
|
<div class="progress-track" id="progressTrack"><div class="progress-fill" id="progressFill"></div></div>
|
|
</div>
|
|
|
|
<!-- SETTINGS PANEL -->
|
|
<div class="settings-overlay" id="settingsOverlay">
|
|
<div class="settings-panel" style="position:relative">
|
|
<button class="close-settings" id="closeSettings">✕</button>
|
|
<h2>⚙️ Settings</h2>
|
|
<div class="setting-group">
|
|
<label>Voice</label>
|
|
<div class="setting-row">
|
|
<select id="voiceSelect"></select>
|
|
</div>
|
|
</div>
|
|
<div class="setting-group">
|
|
<label>Speed</label>
|
|
<div class="setting-row">
|
|
<input type="range" id="speedRange" min="50" max="200" value="100" step="25">
|
|
<span class="speed-value" id="speedValue">1.0x</span>
|
|
</div>
|
|
</div>
|
|
<div class="setting-group">
|
|
<label>Theme</label>
|
|
<div class="setting-row theme-toggle">
|
|
<button class="theme-option active" data-theme="dark">🌙 Dark</button>
|
|
<button class="theme-option" data-theme="light">☀️ Light</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TOAST -->
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
(function(){
|
|
'use strict';
|
|
|
|
/* === STATE === */
|
|
var state={
|
|
text:'',selectedText:'',isPlaying:false,isPaused:false,
|
|
voices:[],voice:null,speed:1,repeat:false,
|
|
utterance:null,charIndex:0,theme:'dark',
|
|
scrollPos:0
|
|
};
|
|
|
|
/* === DOM === */
|
|
var $=function(s){return document.querySelector(s)};
|
|
var welcome=$('#welcome'),app=$('#app');
|
|
var dropzone=$('#dropzone'),fileInput=$('#fileInput'),sampleBtn=$('#sampleBtn');
|
|
var reader=$('#reader'),readerContent=$('#readerContent');
|
|
var floatingPlay=$('#floatingPlay');
|
|
var playbackBar=$('#playbackBar'),trackInfo=$('#trackInfo');
|
|
var playPauseBtn=$('#playPauseBtn'),stopBtn=$('#stopBtn'),repeatBtn=$('#repeatBtn');
|
|
var progressTrack=$('#progressTrack'),progressFill=$('#progressFill');
|
|
var settingsBtn=$('#settingsBtn'),settingsOverlay=$('#settingsOverlay'),closeSettings=$('#closeSettings');
|
|
var voiceSelect=$('#voiceSelect'),speedRange=$('#speedRange'),speedValue=$('#speedValue');
|
|
var themeToggle=$('#themeToggle'),importBtn=$('#importBtn');
|
|
var progressBar=$('#progressBar'),toast=$('#toast');
|
|
|
|
/* === SAMPLE TEXT === */
|
|
var sampleText=[
|
|
"The Art of Shadowing",
|
|
"",
|
|
"Shadowing is a language learning technique where you listen to a piece of audio and repeat it almost simultaneously, like an echo. The goal is not just to understand the words, but to mimic the rhythm, intonation, and natural flow of native speech.",
|
|
"Unlike traditional study methods that focus on grammar rules and vocabulary lists, shadowing trains your mouth muscles to move the way native speakers do. It bridges the gap between understanding English and actually speaking it fluently.",
|
|
"Here's how it works: choose a passage from a book, article, or any text you enjoy. Select the text and press Play. Listen carefully to how each sentence is pronounced — the pauses, the emphasis, the rising and falling pitch. Then repeat it aloud, trying to match the speaker as closely as possible.",
|
|
"Don't worry about perfection at first. Your tongue will feel clumsy, and you'll stumble over words. That's completely normal. The key is consistency. Even ten minutes of focused shadowing every day will produce noticeable results within a few weeks.",
|
|
"Start with slower speeds if needed. Many learners find that 0.75x or 0.8x speed gives them enough time to process and repeat. As you improve, gradually increase to normal speed, then even faster for an extra challenge.",
|
|
"Choose content you genuinely find interesting. If you love science fiction, practice with sci-fi passages. If you're into business, use articles from your field. When the material engages you, practice stops feeling like work.",
|
|
"Remember: every fluent speaker was once a beginner who kept showing up. Press play, open your mouth, and start shadowing."
|
|
].join('\n\n');
|
|
|
|
/* === INIT VOICES === */
|
|
function loadVoices(){
|
|
var vs=speechSynthesis.getVoices();
|
|
if(vs.length>0){
|
|
state.voices=vs.filter(function(v){return v.lang.startsWith('en')});
|
|
populateVoices();
|
|
}
|
|
}
|
|
function populateVoices(){
|
|
voiceSelect.innerHTML='';
|
|
var preferred=null;
|
|
state.voices.forEach(function(v,i){
|
|
var o=document.createElement('option');
|
|
o.value=i;o.textContent=v.name+' ('+v.lang+')';
|
|
if(v.name.match(/Google|Samantha|Alex|Daniel|Karen|Microsoft/))preferred=i;
|
|
voiceSelect.appendChild(o);
|
|
});
|
|
if(preferred!==null&&state.voices[preferred]){state.voice=state.voices[preferred];voiceSelect.value=preferred;}
|
|
else if(state.voices.length>0&&!state.voice){state.voice=state.voices[0];}
|
|
}
|
|
speechSynthesis.onvoiceschanged=loadVoices;
|
|
loadVoices();
|
|
|
|
/* === FILE IMPORT === */
|
|
dropzone.addEventListener('click',function(){fileInput.click();});
|
|
dropzone.addEventListener('dragover',function(e){e.preventDefault();dropzone.classList.add('dragover');});
|
|
dropzone.addEventListener('dragleave',function(){dropzone.classList.remove('dragover');});
|
|
dropzone.addEventListener('drop',function(e){
|
|
e.preventDefault();dropzone.classList.remove('dragover');
|
|
var f=e.dataTransfer.files[0];if(f)handleFile(f);
|
|
});
|
|
fileInput.addEventListener('change',function(e){if(e.target.files[0])handleFile(e.target.files[0]);});
|
|
importBtn.addEventListener('click',function(){fileInput.click();});
|
|
|
|
function handleFile(f){
|
|
var r=new FileReader();
|
|
r.onload=function(){loadText(r.result,f.name.replace(/\.[^.]+$/,''));};
|
|
r.readAsText(f);
|
|
}
|
|
|
|
sampleBtn.addEventListener('click',function(){loadText(sampleText,'Sample Text');});
|
|
|
|
/* === LOAD TEXT INTO READER === */
|
|
function loadText(text,title){
|
|
state.text=text;
|
|
state.selectedText='';
|
|
var paras=text.split(/\n\s*\n/).filter(function(p){return p.trim().length>0;});
|
|
var html='';
|
|
paras.forEach(function(p){
|
|
var clean=p.trim().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
html+='<p>'+clean+'</p>';
|
|
});
|
|
readerContent.innerHTML=html;
|
|
welcome.style.display='none';
|
|
app.classList.add('active');
|
|
document.title=title+' — Shadow Reader';
|
|
showToast('Loaded: '+title);
|
|
|
|
var saved=localStorage.getItem('sr-scroll');
|
|
if(saved){setTimeout(function(){reader.scrollTop=parseInt(saved,10);},100);}
|
|
reader.addEventListener('scroll',saveScroll);
|
|
}
|
|
|
|
function saveScroll(){localStorage.setItem('sr-scroll',reader.scrollTop);}
|
|
|
|
/* === TEXT SELECTION + FLOATING PLAY === */
|
|
document.addEventListener('mouseup',function(e){
|
|
setTimeout(function(){
|
|
var sel=window.getSelection();
|
|
var txt=sel.toString().trim();
|
|
if(txt.length<2){hideFloatingPlay();return;}
|
|
state.selectedText=txt;
|
|
var rng=sel.getRangeAt(0);
|
|
var rect=rng.getBoundingClientRect();
|
|
floatingPlay.style.left=(rect.left+rect.width/2)+'px';
|
|
floatingPlay.style.top=rect.top+'px';
|
|
floatingPlay.classList.add('visible');
|
|
},10);
|
|
});
|
|
|
|
floatingPlay.addEventListener('click',function(){hideFloatingPlay();startPlayback(state.selectedText);});
|
|
document.addEventListener('mousedown',function(e){
|
|
if(!floatingPlay.contains(e.target))hideFloatingPlay();
|
|
});
|
|
|
|
function hideFloatingPlay(){floatingPlay.classList.remove('visible');}
|
|
|
|
/* === TTS PLAYBACK === */
|
|
function startPlayback(text){
|
|
if(!text||!text.trim())return;
|
|
speechSynthesis.cancel();
|
|
state.isPlaying=true;state.isPaused=false;state.charIndex=0;
|
|
|
|
var u=new SpeechSynthesisUtterance(text);
|
|
if(state.voice)u.voice=state.voice;
|
|
u.rate=state.speed;
|
|
u.pitch=1;
|
|
u.lang=state.voice?state.voice.lang:'en-US';
|
|
|
|
u.onstart=function(){
|
|
updatePlaybackUI(true);
|
|
trackInfo.textContent=text.length>60?text.substring(0,57)+'...':text;
|
|
};
|
|
u.onend=function(){
|
|
if(state.repeat&&!state.isPaused){
|
|
setTimeout(function(){startPlayback(text);},800);
|
|
return;
|
|
}
|
|
state.isPlaying=false;
|
|
updatePlaybackUI(false);
|
|
};
|
|
u.onerror=function(){state.isPlaying=false;updatePlaybackUI(false);showToast('Playback error');};
|
|
u.onboundary=function(e){
|
|
if(e.name==='word'){state.charIndex=e.charIndex;updateProgress();}
|
|
};
|
|
|
|
state.utterance=u;
|
|
speechSynthesis.speak(u);
|
|
}
|
|
|
|
function togglePause(){
|
|
if(!state.utterance)return;
|
|
if(state.isPaused){
|
|
speechSynthesis.resume();
|
|
state.isPaused=false;
|
|
playPauseBtn.textContent='⏸️';
|
|
}else{
|
|
speechSynthesis.pause();
|
|
state.isPaused=true;
|
|
playPauseBtn.textContent='▶️';
|
|
}
|
|
}
|
|
|
|
function stopPlayback(){
|
|
speechSynthesis.cancel();
|
|
state.isPlaying=false;state.isPaused=false;
|
|
updatePlaybackUI(false);
|
|
}
|
|
|
|
function updatePlaybackUI(playing){
|
|
playbackBar.classList.toggle('active',playing);
|
|
playPauseBtn.textContent=playing?(state.isPaused?'▶️':'⏸️'):'▶️';
|
|
progressFill.style.width=playing?'0%':'0%';
|
|
if(!playing)trackInfo.textContent='Select text to play';
|
|
}
|
|
|
|
function updateProgress(){
|
|
if(!state.utterance)return;
|
|
var total=state.utterance.text.length;
|
|
var pct=Math.min(100,(state.charIndex/total)*100);
|
|
progressFill.style.width=pct+'%';
|
|
}
|
|
|
|
playPauseBtn.addEventListener('click',togglePause);
|
|
stopBtn.addEventListener('click',stopPlayback);
|
|
repeatBtn.addEventListener('click',function(){
|
|
state.repeat=!state.repeat;
|
|
repeatBtn.style.background=state.repeat?'var(--accent)':'var(--border)';
|
|
repeatBtn.style.color=state.repeat?'var(--ink)':'var(--text-muted)';
|
|
showToast(state.repeat?'Repeat ON':'Repeat OFF');
|
|
});
|
|
|
|
/* === SETTINGS === */
|
|
settingsBtn.addEventListener('click',function(){settingsOverlay.classList.add('open');});
|
|
closeSettings.addEventListener('click',function(){settingsOverlay.classList.remove('open');});
|
|
settingsOverlay.addEventListener('click',function(e){if(e.target===settingsOverlay)settingsOverlay.classList.remove('open');});
|
|
|
|
voiceSelect.addEventListener('change',function(){
|
|
state.voice=state.voices[parseInt(this.value,10)]||null;
|
|
localStorage.setItem('sr-voice',this.value);
|
|
});
|
|
|
|
speedRange.addEventListener('input',function(){
|
|
state.speed=this.value/100;
|
|
speedValue.textContent=state.speed.toFixed(2)+'x';
|
|
});
|
|
speedRange.addEventListener('change',function(){
|
|
localStorage.setItem('sr-speed',this.value);
|
|
});
|
|
|
|
/* THEME */
|
|
function setTheme(t){
|
|
state.theme=t;
|
|
document.documentElement.setAttribute('data-theme',t);
|
|
localStorage.setItem('sr-theme',t);
|
|
document.querySelectorAll('.theme-option').forEach(function(b){
|
|
b.classList.toggle('active',b.dataset.theme===t);
|
|
});
|
|
themeToggle.textContent=t==='dark'?'☀️':'🌓';
|
|
}
|
|
themeToggle.addEventListener('click',function(){setTheme(state.theme==='dark'?'light':'dark');});
|
|
document.querySelectorAll('.theme-option').forEach(function(b){
|
|
b.addEventListener('click',function(){setTheme(b.dataset.theme);});
|
|
});
|
|
|
|
/* === RESTORE SETTINGS === */
|
|
(function restore(){
|
|
var t=localStorage.getItem('sr-theme')||'dark';setTheme(t);
|
|
var s=localStorage.getItem('sr-speed')||'100';speedRange.value=s;state.speed=s/100;speedValue.textContent=state.speed.toFixed(2)+'x');
|
|
var v=localStorage.getItem('sr-voice');
|
|
})();
|
|
|
|
/* === READ PROGRESS === */
|
|
reader.addEventListener('scroll',function(){
|
|
var pct=Math.round((reader.scrollTop/(reader.scrollHeight-reader.clientHeight))*100)||0;
|
|
progressBar.style.width=pct+'%';
|
|
});
|
|
|
|
/* === TOAST === */
|
|
function showToast(msg){
|
|
toast.textContent=msg;toast.classList.add('show');
|
|
clearTimeout(showToast._t);
|
|
showToast._t=setTimeout(function(){toast.classList.remove('show');},2200);
|
|
}
|
|
|
|
/* KEYBOARD SHORTCUTS */
|
|
document.addEventListener('keydown',function(e){
|
|
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT')return;
|
|
if(e.code==='Space'&&state.isPlaying){e.preventDefault();togglePause();}
|
|
if(e.code==='Escape'){stopPlayback();settingsOverlay.classList.remove('open');}
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|