2026-10-10 06:50:45 +00:00
|
|
|
import { Countdown, integer, adjustTime } from './timer.mjs';
|
2026-10-10 06:56:14 +00:00
|
|
|
import { SETTINGS_KEY, serializeSettings, parseSettings } from './settings.mjs';
|
2026-10-10 06:45:15 +00:00
|
|
|
|
|
|
|
|
const timer = new Countdown();
|
|
|
|
|
const minutes = document.querySelector('#minutes');
|
|
|
|
|
const seconds = document.querySelector('#seconds');
|
|
|
|
|
const counter = document.querySelector('.counter');
|
|
|
|
|
const startStop = document.querySelector('#start-stop');
|
|
|
|
|
const reset = document.querySelector('#reset');
|
|
|
|
|
const status = document.querySelector('#status');
|
|
|
|
|
const fields = [minutes, seconds];
|
2026-10-10 06:56:14 +00:00
|
|
|
const title = document.title;
|
2026-10-10 06:45:15 +00:00
|
|
|
let ticker;
|
|
|
|
|
|
2026-10-10 06:56:14 +00:00
|
|
|
function saveSettings() {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(SETTINGS_KEY, serializeSettings(minutes.value, seconds.value));
|
|
|
|
|
} catch {
|
|
|
|
|
// The timer still works when storage is disabled or full.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 06:45:15 +00:00
|
|
|
function resize(field) {
|
|
|
|
|
field.style.width = `${Math.max(field === seconds ? 2 : 1, field.value.length)}ch`;
|
|
|
|
|
field.dataset.valid = field.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function render(updateFields = true) {
|
|
|
|
|
if (updateFields) {
|
|
|
|
|
const display = timer.display();
|
|
|
|
|
minutes.value = display.minutes;
|
|
|
|
|
seconds.value = display.seconds;
|
|
|
|
|
}
|
|
|
|
|
fields.forEach(field => {
|
|
|
|
|
field.readOnly = timer.state === 'running';
|
|
|
|
|
resize(field);
|
|
|
|
|
});
|
|
|
|
|
counter.classList.toggle('finished', timer.finished);
|
2026-10-10 06:56:14 +00:00
|
|
|
document.title = timer.finished ? `🔔 ${title}` : title;
|
2026-10-10 06:45:15 +00:00
|
|
|
startStop.textContent = timer.state === 'running' ? 'Stop' : 'Start';
|
|
|
|
|
const message = timer.finished ? 'Countdown finished' :
|
|
|
|
|
timer.state === 'running' ? 'Running' :
|
|
|
|
|
timer.state === 'paused' ? 'Paused' : 'Idle';
|
|
|
|
|
if (status.textContent !== message) status.textContent = message;
|
|
|
|
|
if (timer.state !== 'running') {
|
|
|
|
|
clearInterval(ticker);
|
|
|
|
|
ticker = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function edited() {
|
|
|
|
|
timer.edit(minutes.value, seconds.value);
|
2026-10-10 06:56:14 +00:00
|
|
|
saveSettings();
|
2026-10-10 06:45:15 +00:00
|
|
|
render(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const field of fields) {
|
|
|
|
|
resize(field);
|
|
|
|
|
field.addEventListener('input', () => {
|
|
|
|
|
if (field.readOnly || !/^\d*$/.test(field.value)) {
|
|
|
|
|
field.value = field.dataset.valid;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
edited();
|
|
|
|
|
});
|
|
|
|
|
field.addEventListener('keydown', event => {
|
|
|
|
|
if (field.readOnly || !['ArrowUp', 'ArrowDown'].includes(event.key)) return;
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const step = event.shiftKey ? 10n : 1n;
|
2026-10-10 06:50:45 +00:00
|
|
|
const adjusted = adjustTime(minutes.value, seconds.value, field.id,
|
|
|
|
|
event.key === 'ArrowUp' ? step : -step);
|
|
|
|
|
minutes.value = adjusted.minutes;
|
|
|
|
|
seconds.value = adjusted.seconds;
|
2026-10-10 06:45:15 +00:00
|
|
|
edited();
|
|
|
|
|
});
|
|
|
|
|
field.addEventListener('blur', () => {
|
|
|
|
|
if (field.readOnly) return;
|
|
|
|
|
field.value = String(integer(field.value));
|
|
|
|
|
if (field === seconds) field.value = field.value.padStart(2, '0');
|
|
|
|
|
resize(field);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startStop.addEventListener('click', () => {
|
|
|
|
|
timer.toggle();
|
|
|
|
|
render();
|
|
|
|
|
if (timer.state === 'running' && ticker === undefined) {
|
|
|
|
|
// This is only a display refresh; elapsed time comes from the deadline.
|
|
|
|
|
ticker = setInterval(() => {
|
|
|
|
|
timer.update();
|
|
|
|
|
render();
|
|
|
|
|
}, 50);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
reset.addEventListener('click', () => {
|
|
|
|
|
timer.reset();
|
|
|
|
|
render();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
|
|
|
if (timer.state === 'running') {
|
|
|
|
|
timer.update();
|
|
|
|
|
render();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-10 06:56:14 +00:00
|
|
|
try {
|
|
|
|
|
const saved = parseSettings(localStorage.getItem(SETTINGS_KEY));
|
|
|
|
|
minutes.value = saved.minutes;
|
|
|
|
|
seconds.value = saved.seconds;
|
|
|
|
|
timer.edit(minutes.value, seconds.value);
|
|
|
|
|
} catch {
|
|
|
|
|
// Storage may be unavailable; retain the default idle countdown.
|
|
|
|
|
}
|
|
|
|
|
render(false);
|