import { getTheme, toggleTheme, onThemeChange } from '../theme.js';
import { logoLockup } from '../logo.js';
import { themeIcon } from './icons.js';
import { escapeHtml } from '../util.js';
const PACE_WPM = 130;
const LINE_GAP_SECONDS = 0.6;
function buildSchedule(chunks) {
const allTimed = chunks.every(c => c.time !== null && c.time !== undefined);
if (allTimed) {
const base = chunks[0].time;
return chunks.map(c => c.time - base);
}
let t = 0;
const offsets = [];
for (const c of chunks) {
offsets.push(t);
const duration = (c.content.length / 5) / (PACE_WPM / 60);
t += duration + LINE_GAP_SECONDS;
}
return offsets;
}
function renderLine(content, typed) {
const inputChars = typed.split('');
let html = '';
for (let i = 0; i < content.length; i++) {
const char = content[i];
const inputChar = inputChars[i];
let cls = 'neutral';
let displayChar = char;
if (i < inputChars.length) {
cls = inputChar === char ? 'correct' : 'incorrect';
if (cls === 'incorrect') displayChar = inputChar;
}
html += `${escapeHtml(displayChar)}`;
}
return html;
}
export function renderPassiveMode(root, { name, chunks, onExit }) {
const schedule = buildSchedule(chunks);
const totalDuration = schedule[schedule.length - 1] + 4; // let the last line linger
root.innerHTML = `
`;
const prevLine = root.querySelector('.passive-line.prev');
const activeLine = root.querySelector('.passive-line.active');
const nextLine = root.querySelector('.passive-line.next');
const input = root.querySelector('.passive-input');
const accField = root.querySelector('[data-field="acc"]');
const summary = root.querySelector('.passive-summary');
const themeButton = root.querySelector('.theme-toggle');
const paintTheme = () => { themeButton.innerHTML = themeIcon(getTheme()); };
paintTheme();
const unsubscribeTheme = onThemeChange(paintTheme);
themeButton.addEventListener('click', toggleTheme);
root.querySelector('[data-action="menu"]').addEventListener('click', onExit);
let activeIndex = -1;
let typedForActive = '';
let totalCorrect = 0;
let totalTyped = 0;
let done = false;
const startedAt = Date.now();
let intervalId = null;
function paintLines() {
prevLine.textContent = activeIndex > 0 ? chunks[activeIndex - 1].content : '';
activeLine.innerHTML = activeIndex >= 0 ? renderLine(chunks[activeIndex].content, typedForActive) : '';
nextLine.textContent = activeIndex + 1 < chunks.length ? chunks[activeIndex + 1].content : '';
}
function paintAccuracy() {
const acc = totalTyped === 0 ? 100 : Math.round((totalCorrect / totalTyped) * 100);
accField.textContent = `${acc}%`;
return acc;
}
function finishSession() {
if (done) return;
done = true;
if (intervalId) clearInterval(intervalId);
input.disabled = true;
const acc = paintAccuracy();
summary.querySelector('[data-field="lines"]').textContent = String(chunks.length);
summary.querySelector('[data-field="final-acc"]').textContent = `${acc}%`;
summary.hidden = false;
}
function advanceTo(index) {
// Score whatever was typed for the outgoing line before moving on.
if (activeIndex >= 0) {
const target = chunks[activeIndex].content;
for (let i = 0; i < typedForActive.length; i++) {
totalTyped++;
if (typedForActive[i] === target[i]) totalCorrect++;
}
}
activeIndex = index;
typedForActive = '';
input.value = '';
paintLines();
paintAccuracy();
}
input.addEventListener('input', () => {
if (done || activeIndex < 0) return;
typedForActive = input.value;
activeLine.innerHTML = renderLine(chunks[activeIndex].content, typedForActive);
});
input.addEventListener('blur', () => {
if (!done) setTimeout(() => input.focus(), 10);
});
summary.querySelector('[data-action="back"]').addEventListener('click', onExit);
setTimeout(() => input.focus(), 50);
advanceTo(0);
intervalId = setInterval(() => {
if (done) return;
const elapsed = (Date.now() - startedAt) / 1000;
let target = activeIndex;
while (target + 1 < schedule.length && elapsed >= schedule[target + 1]) target++;
if (target !== activeIndex) advanceTo(target);
if (elapsed >= totalDuration) finishSession();
}, 150);
return () => {
unsubscribeTheme();
if (intervalId) clearInterval(intervalId);
};
}