<div id="lrd-app-body">
<style scoped>
#lrd-app-body {
font-family: 'DM Sans', sans-serif;
background: #FDFAF5;
min-height: 100vh;
padding-bottom: 60px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#FDFAF5;--bg2:#F4EFE5;--bg3:#EDE6D6;--card:#FFFFFF;
--text:#1A1612;--text2:#6B6055;--text3:#A09080;
--border:rgba(90,70,50,0.12);--border2:rgba(90,70,50,0.22);
--blue:#2B6CB0;--blue-bg:#EBF4FF;
--green:#276749;--green-bg:#E6F4EC;
--amber:#92400E;--amber-bg:#FEF3C7;
--purple:#553C9A;--purple-bg:#EBE5FF;
--radius:16px;--radius-sm:10px;
}
#lrd-app-body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding-bottom:60px}
.topbar{background:var(--card);border-bottom:0.5px solid var(--border);padding:16px 20px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10}
.topbar-title{font-family:'Fraunces',serif;font-size:22px;font-weight:600;color:var(--text);letter-spacing:-0.02em}
.topbar-sub{font-size:13px;color:var(--text3)}
.container{max-width:560px;margin:0 auto;padding:24px 16px}
.card{background:var(--card);border:0.5px solid var(--border);border-radius:var(--radius);padding:22px;margin-bottom:16px}
.screen{display:none}.screen.active{display:block}
h2{font-family:'Fraunces',serif;font-size:24px;font-weight:500;color:var(--text);margin-bottom:8px;letter-spacing:-0.02em}
h3{font-size:16px;font-weight:500;color:var(--text);margin-bottom:10px}
p{font-size:15px;color:var(--text2);line-height:1.6;margin-bottom:10px}
.hint{font-size:13px;color:var(--text3);margin-top:4px;line-height:1.5}
textarea{width:100%;padding:12px 14px;font-size:14px;border:0.5px solid var(--border2);border-radius:var(--radius-sm);background:var(--bg);color:var(--text);font-family:monospace;line-height:1.7;outline:none;resize:vertical;min-height:160px}
.file-drop{border:1.5px dashed var(--border2);border-radius:var(--radius-sm);padding:26px;text-align:center;cursor:pointer;transition:background 0.15s,border-color 0.15s;margin-bottom:12px}
.file-drop:hover,.file-drop.dragover{background:var(--bg2);border-color:var(--blue)}
.file-drop input{display:none}
.file-loaded{font-size:14px;color:var(--green);font-weight:500;margin-top:8px}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:14px 22px;font-size:16px;font-weight:500;border:0.5px solid var(--border2);border-radius:var(--radius-sm);background:var(--card);color:var(--text);cursor:pointer;transition:background 0.15s,transform 0.1s;font-family:'DM Sans',sans-serif;white-space:nowrap}
.btn:hover{background:var(--bg2)}.btn:active{transform:scale(0.97)}
.btn:disabled{opacity:0.35;cursor:not-allowed;transform:none;pointer-events:none}
.btn-primary{background:var(--text);color:var(--bg);border-color:var(--text)}.btn-primary:hover{background:#3D342B}
.btn-green{background:var(--green);color:white;border-color:var(--green)}.btn-green:hover{background:#1D4E38}
.btn-blue{background:var(--blue);color:white;border-color:var(--blue)}.btn-blue:hover{background:#1A4A87}
.btn-purple{background:var(--purple);color:white;border-color:var(--purple)}.btn-purple:hover{background:#3B2780}
.btn-full{width:100%}.btn-xl{padding:18px 28px;font-size:18px;border-radius:var(--radius)}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.divider{height:0.5px;background:var(--border);margin:16px 0}
.voice-pick{display:flex;gap:10px;margin-bottom:16px}
.vbtn{flex:1;padding:14px 10px;border:2px solid var(--border2);border-radius:var(--radius-sm);text-align:center;cursor:pointer;transition:all 0.15s;background:var(--bg)}
.vbtn.sel-f{border-color:var(--green);background:var(--green-bg)}
.vbtn.sel-m{border-color:var(--blue);background:var(--blue-bg)}
.vbtn-icon{font-size:28px;margin-bottom:6px}
.vbtn-label{font-size:15px;font-weight:500}
.vbtn.sel-f .vbtn-label{color:var(--green)}
.vbtn.sel-m .vbtn-label{color:var(--blue)}
.progress-wrap{margin-bottom:18px}
.progress-track{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
.progress-fill{height:100%;background:var(--text);border-radius:3px;transition:width 0.4s ease}
.progress-label{display:flex;justify-content:space-between;margin-bottom:8px}
.progress-label span{font-size:13px;color:var(--text3)}
.stage{background:var(--card);border-radius:var(--radius);border:0.5px solid var(--border);padding:32px 24px;text-align:center;margin-bottom:16px;min-height:200px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.stage.memo{border-top:4px solid var(--purple)}
.stage-num{font-size:12px;font-weight:500;letter-spacing:0.08em;text-transform:uppercase;color:var(--purple);margin-bottom:14px}
.stage-text{font-family:'Fraunces',serif;font-size:26px;font-weight:400;line-height:1.55;color:var(--text);transition:filter 0.3s}
.stage-text.blurred{filter:blur(10px);user-select:none}
.wave-row{display:flex;align-items:center;justify-content:center;gap:5px;height:44px;margin:8px 0}
.wave-bar{width:5px;background:var(--purple);border-radius:3px;animation:wavePulse 0.7s ease-in-out infinite}
.wave-bar:nth-child(1){animation-delay:0s}.wave-bar:nth-child(2){animation-delay:0.1s}
.wave-bar:nth-child(3){animation-delay:0.2s}.wave-bar:nth-child(4){animation-delay:0.3s}
.wave-bar:nth-child(5){animation-delay:0.2s}.wave-bar:nth-child(6){animation-delay:0.1s}
@keyframes wavePulse{0%,100%{height:8px;opacity:0.3}50%{height:32px;opacity:1}}
.wave-row.idle .wave-bar{animation:none;height:8px;opacity:0.3}
.status{font-size:14px;color:var(--text3);text-align:center;min-height:22px;margin-bottom:12px}
.status.speaking{color:var(--purple)}.status.kid-turn{color:var(--green);font-weight:500}.status.err{color:#9B2C2C}
.memo-prompt{background:var(--purple-bg);border-radius:var(--radius-sm);padding:14px 18px;text-align:center;margin-bottom:14px;border:0.5px solid rgba(85,60,154,0.2)}
.memo-prompt p{color:var(--purple);font-size:15px;font-weight:500;margin:0}
.controls{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:10px}
.finish-state{text-align:center;padding:24px 0}
.finish-icon{font-size:64px;margin-bottom:14px}
.finish-title{font-family:'Fraunces',serif;font-size:28px;font-weight:500;margin-bottom:10px}
.finish-sub{font-size:16px;color:var(--text2)}
.speed-row{margin-top:14px}
.speed-row label{font-size:14px;color:var(--text2);font-weight:500;display:block;margin-bottom:6px}
@media(max-width:480px){.stage-text{font-size:22px}.container{padding:16px 12px}}
</style>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;500;600&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet">
<div class="topbar">
<div class="topbar-title">📖 Textalærdómur</div>
<div class="container">
<!-- SCREEN 1: SETUP -->
<div class="screen active" id="screen-setup">
<div style="margin-bottom:20px">
<h2>Hvernig viltu læra?</h2>
<p>Appið les eina línu — þú endurtekur hana. Smelltu svo á næstu.</p>
</div>
<div class="card">
<h3>Rödd</h3>
<div class="voice-pick">
<div class="vbtn sel-f" id="vbtn-f" onclick="setVoice('f')">
<div class="vbtn-icon">👩</div>
<div class="vbtn-label">Kona</div>
</div>
<div class="vbtn" id="vbtn-m" onclick="setVoice('m')">
<div class="vbtn-icon">👨</div>
<div class="vbtn-label">Karl</div>
</div>
</div>
<div class="speed-row">
<label>Hraði — <span id="speed-label">eðlilegur (1.0)</span></label>
<input type="range" id="speed-slider" min="0.6" max="1.5" step="0.1" value="1.0"
oninput="updateSpeed(this.value)"
style="width:100%;accent-color:var(--text)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text3);margin-top:4px">
<span>Hægt</span><span>Eðlilegt</span><span>Hratt</span>
</div>
</div>
</div>
<div class="card" style="border:2px solid var(--green);background:var(--green-bg)">
<h3 style="color:var(--green)">⚡ Byrja strax</h3>
<p style="color:var(--green);margin-bottom:14px">Faðirvor er þegar hlaðið inn — smelltu bara hér!</p>
<button class="btn btn-green btn-full btn-xl" onclick="startFadirvor()">🙏 Æfa Faðirvor</button>
</div>
<div class="card">
<h3>Texti til að læra</h3>
<div class="file-drop" id="file-drop" onclick="document.getElementById('file-input').click()">
<input type="file" id="file-input" accept=".txt,.text"/>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color:var(--text3);margin-bottom:10px"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div style="font-size:15px;color:var(--text2)">Veldu .txt skrá</div>
<div style="font-size:13px;color:var(--text3);margin-top:4px">eða dragðu hér</div>
</div>
<div id="file-loaded" style="display:none" class="file-loaded"></div>
<div class="divider"></div>
<p style="margin:0 0 8px;font-size:14px;color:var(--text2)">Eða límdu textann hér:</p>
<textarea id="paste-input" placeholder="Faðir vor, sem ert á himnum,
helgist þitt nafn,
til komi þitt ríki,
verði þinn vilji..."></textarea>
</div>
<button class="btn btn-primary btn-full btn-xl" onclick="startMemo()">Byrja æfingu →</button>
</div>
<!-- SCREEN 2: PRACTICE -->
<div class="screen" id="screen-practice">
<div class="progress-wrap">
<div class="progress-label">
<span id="prog-label">Lína 1 / 1</span>
<span id="prog-pct">0%</span>
</div>
<div class="progress-track"><div class="progress-fill" id="prog-fill" style="width:0%"></div></div>
</div>
<div class="stage memo" id="stage">
<div class="stage-num" id="stage-num">Lína 1</div>
<div class="stage-text blurred" id="stage-text">—</div>
</div>
<div id="wave-wrap" style="display:none">
<div class="wave-row idle" id="wave-row">
<div class="wave-bar"></div><div class="wave-bar"></div>
<div class="wave-bar"></div><div class="wave-bar"></div>
<div class="wave-bar"></div><div class="wave-bar"></div>
</div>
</div>
<div class="status" id="status">Hlusta...</div>
<div class="memo-prompt">
<p>🔁 Endurtaktu línuna — svo farðu áfram</p>
</div>
<div class="controls">
<button class="btn btn-purple" onclick="replayLine()">
<svg width="15" height="15" viewBox="0 0 14 14" fill="currentColor"><path d="M2 1.5l11 5.5-11 5.5z"/></svg>
Endurtaka
</button>
<button class="btn btn-green" onclick="revealLine()">Sýna línu</button>
<button class="btn btn-primary btn-xl" id="btn-next" onclick="nextLine()">Næsta →</button>
</div>
<div style="margin-top:28px;text-align:center">
<button class="btn" style="font-size:13px;padding:8px 16px;color:var(--text3)" onclick="showScreen('screen-setup','Veldu texta')">← Til baka</button>
</div>
</div>
</div>
<script>
const API_KEY = 'AIzaSyD4it8o1iU1fqleyI-7LciN28UwKl0yxN4';
let lines = [];
let currentIdx = 0;
let speaking = false;
let voiceGender = 'f';
let speechRate = 1.0;
let currentLineText = '';
// ── Voice/speed ────────────────────────────────────────
function setVoice(g) {
voiceGender = g;
const f = document.getElementById('vbtn-f');
const m = document.getElementById('vbtn-m');
f.className = 'vbtn' + (g==='f' ? ' sel-f' : '');
m.className = 'vbtn' + (g==='m' ? ' sel-m' : '');
}
function updateSpeed(v) {
speechRate = parseFloat(v);
const labels = {'0.6':'mjög hægt','0.7':'hægt','0.8':'nokkuð hægt','0.9':'aðeins hægt','1.0':'eðlilegt','1.1':'aðeins hratt','1.2':'frekar hratt','1.3':'hratt','1.4':'mjög hratt','1.5':'hámark'};
const el = document.getElementById('speed-label');
if (el) el.textContent = (labels[parseFloat(v).toFixed(1)] || v) + ' (' + parseFloat(v).toFixed(1) + ')';
}
// ── File loading ───────────────────────────────────────
const fileDrop = document.getElementById('file-drop');
const fileInput = document.getElementById('file-input');
fileDrop.addEventListener('dragover', e=>{e.preventDefault();fileDrop.classList.add('dragover');});
fileDrop.addEventListener('dragleave', ()=>fileDrop.classList.remove('dragover'));
fileDrop.addEventListener('drop', e=>{e.preventDefault();fileDrop.classList.remove('dragover');const f=e.dataTransfer.files[0];if(f)loadFile(f);});
fileInput.addEventListener('change', e=>{if(e.target.files[0])loadFile(e.target.files[0]);});
function hasGarbled(t){return t.includes('\uFFFD')||/[ÃðþæöáéíóúýÐÞÆÖÁÉÍÓÚÝ]/.test(t);}
function loadFile(f){
const tryRead=(enc,fb)=>{
const r=new FileReader();
r.onload=ev=>{
const t=ev.target.result;
if(fb&&hasGarbled(t)){tryRead(fb,null);return;}
document.getElementById('paste-input').value=t;
document.getElementById('file-loaded').style.display='block';
document.getElementById('file-loaded').textContent='✓ '+f.name+' hlaðið upp';
};
r.readAsText(f,enc);
};
tryRead('UTF-8','windows-1252');
}
// ── Start ──────────────────────────────────────────────
// Faðirvor baked in
const FADIRVOR_LINES = ["Faðir vor, sem ert á himnum,","helgist þitt nafn,","til komi þitt ríki,","verði þinn vilji,","svo á jörðu sem á himni.","Gef oss í dag vort daglegt brauð,","og fyrirgef oss vorar skuldir,","svo sem vér og fyrirgefum vorum skuldunautum,","og eigi leið oss í freistni,","heldur frelsa oss frá illu.","Því að þitt er ríkið, mátturinn og dýrðin","að eilífu.","Amen."];
function startMemo() {
const raw = document.getElementById('paste-input').value.trim();
lines = raw ? raw.split('\n').map(l=>l.trim()).filter(Boolean) : FADIRVOR_LINES;
if (lines.length === 0) { alert('Enginn texti fannst.'); return; }
currentIdx = 0;
speaking = false;
showScreen('screen-practice', 'Æfing');
renderLine();
}
function startFadirvor() {
lines = FADIRVOR_LINES;
currentIdx = 0;
speaking = false;
showScreen('screen-practice', 'Æfing');
renderLine();
}
// ── Render ─────────────────────────────────────────────
function renderLine() {
if (currentIdx >= lines.length) { showFinish(); return; }
currentLineText = lines[currentIdx];
const total = lines.length;
const pct = Math.round((currentIdx / total) * 100);
document.getElementById('prog-fill').style.width = pct + '%';
document.getElementById('prog-label').textContent = 'Lína ' + (currentIdx+1) + ' / ' + total;
document.getElementById('prog-pct').textContent = pct + '%';
document.getElementById('stage-num').textContent = 'Lína ' + (currentIdx+1);
document.getElementById('stage-text').textContent = currentLineText;
document.getElementById('stage-text').classList.add('blurred');
document.getElementById('wave-wrap').style.display = 'none';
document.getElementById('btn-next').disabled = false;
setStatus('Hlusta...', 'speaking');
setTimeout(() => playLine(), 400);
}
function revealLine() {
document.getElementById('stage-text').classList.remove('blurred');
}
function replayLine() {
playLine();
}
function nextLine() {
currentIdx++;
renderLine();
}
// ── TTS ────────────────────────────────────────────────
async function playLine() {
if (speaking) return;
speaking = true;
setWave(true);
setStatus('Les upphátt...', 'speaking');
const waveNet = voiceGender==='f' ? 'is-IS-Wavenet-A' : 'is-IS-Wavenet-B';
const standard = voiceGender==='f' ? 'is-IS-Standard-A' : 'is-IS-Standard-B';
const tryVoice = async (vName) => {
const res = await fetch('https://texttospeech.googleapis.com/v1/text:synthesize?key=' + encodeURIComponent(API_KEY), {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
input: {text: currentLineText},
voice: {languageCode:'is-IS', name:vName},
audioConfig: {audioEncoding:'MP3', speakingRate:speechRate, pitch:voiceGender==='m'?-2:0}
})
});
return res.json();
};
try {
let data = await tryVoice(waveNet);
if (!data.audioContent) data = await tryVoice(standard);
if (!data.audioContent && standard === 'is-IS-Standard-B') data = await tryVoice('is-IS-Standard-A');
if (data.audioContent) {
await playMp3(data.audioContent);
} else {
const msg = data.error && data.error.message ? data.error.message : 'Óþekkt villa';
setStatus('Villa: ' + msg, 'err');
}
} catch(e) {
console.error(e);
setStatus('Tenging mistókst', 'err');
}
speaking = false;
setWave(false);
setStatus('Endurtaktu línuna', 'kid-turn');
}
function playMp3(b64) {
return new Promise(resolve => {
const a = new Audio('data:audio/mp3;base64,' + b64);
a.onended = resolve; a.onerror = resolve;
a.play().catch(resolve);
});
}
// ── Finish ─────────────────────────────────────────────
function showFinish() {
const stage = document.getElementById('stage');
stage.className = 'stage';
stage.innerHTML = '<div class="finish-state"><div class="finish-icon">🌟</div><div class="finish-title">Frábært!</div><div class="finish-sub">Þú kláraðir allan textann!</div></div>';
document.getElementById('prog-fill').style.width = '100%';
document.getElementById('prog-pct').textContent = '100%';
document.getElementById('wave-wrap').style.display = 'none';
document.querySelector('.memo-prompt').style.display = 'none';
document.querySelector('.controls').style.display = 'none';
setStatus('');
}
function showScreen(id, label) {
document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
document.getElementById(id).classList.add('active');
document.getElementById('topbar-sub').textContent = label || '';
window.scrollTo(0, 0);
}
function setStatus(msg, type) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status' + (type ? ' '+type : '');
}
function setWave(on) {
document.getElementById('wave-wrap').style.display = on ? 'block' : 'none';
document.getElementById('wave-row').className = 'wave-row' + (on ? '' : ' idle');
}
</script>
</div>