115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
function playCompletionChime() {
|
|
try {
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
if (!AudioContext) return;
|
|
const context = new AudioContext();
|
|
const oscillator = context.createOscillator();
|
|
const gain = context.createGain();
|
|
oscillator.type = "sine";
|
|
oscillator.frequency.setValueAtTime(523.25, context.currentTime);
|
|
oscillator.frequency.setValueAtTime(659.25, context.currentTime + 0.12);
|
|
gain.gain.setValueAtTime(0.0001, context.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.12, context.currentTime + 0.02);
|
|
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35);
|
|
oscillator.connect(gain);
|
|
gain.connect(context.destination);
|
|
oscillator.start();
|
|
oscillator.stop(context.currentTime + 0.36);
|
|
oscillator.addEventListener("ended", () => context.close());
|
|
} catch (_error) {
|
|
// Audio feedback is optional; browser policy must never block completion.
|
|
}
|
|
}
|
|
|
|
function initializeTimer(timer) {
|
|
const duration = Number(timer.dataset.duration);
|
|
const taskId = timer.dataset.taskId;
|
|
const storageKey = `steady.timer.${taskId}`;
|
|
const display = timer.querySelector(".timer__display");
|
|
const progress = timer.querySelector(".timer__progress span");
|
|
const announcement = timer.querySelector(".timer__announcement");
|
|
let intervalId;
|
|
|
|
function readState() {
|
|
try {
|
|
const stored = JSON.parse(localStorage.getItem(storageKey));
|
|
if (stored && Number.isFinite(stored.remaining)) return stored;
|
|
} catch (_error) {
|
|
// A corrupt or unavailable local store simply starts a fresh timer.
|
|
}
|
|
return { remaining: duration, running: false, updatedAt: Date.now() };
|
|
}
|
|
|
|
let state = readState();
|
|
|
|
function currentRemaining() {
|
|
if (!state.running) return state.remaining;
|
|
const elapsed = Math.floor((Date.now() - state.updatedAt) / 1000);
|
|
return Math.max(0, state.remaining - elapsed);
|
|
}
|
|
|
|
function saveState() {
|
|
try {
|
|
localStorage.setItem(storageKey, JSON.stringify(state));
|
|
} catch (_error) {
|
|
// The countdown still works for this page when storage is unavailable.
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
const remaining = currentRemaining();
|
|
const minutes = Math.floor(remaining / 60);
|
|
const seconds = remaining % 60;
|
|
display.textContent = `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
progress.style.width = `${Math.max(0, Math.min(100, (remaining / duration) * 100))}%`;
|
|
|
|
if (remaining === 0 && state.running) {
|
|
state = { remaining: 0, running: false, updatedAt: Date.now() };
|
|
saveState();
|
|
clearInterval(intervalId);
|
|
timer.classList.add("timer--complete");
|
|
announcement.textContent = "Focus block complete. Take a breath.";
|
|
if (timer.dataset.chime === "true") playCompletionChime();
|
|
}
|
|
}
|
|
|
|
timer.querySelector("[data-timer-start]").addEventListener("click", () => {
|
|
const remaining = currentRemaining() || duration;
|
|
state = { remaining, running: true, updatedAt: Date.now() };
|
|
timer.classList.remove("timer--complete");
|
|
saveState();
|
|
clearInterval(intervalId);
|
|
intervalId = window.setInterval(render, 250);
|
|
render();
|
|
});
|
|
|
|
timer.querySelector("[data-timer-pause]").addEventListener("click", () => {
|
|
state = {
|
|
remaining: currentRemaining(),
|
|
running: false,
|
|
updatedAt: Date.now(),
|
|
};
|
|
saveState();
|
|
clearInterval(intervalId);
|
|
render();
|
|
});
|
|
|
|
timer.querySelector("[data-timer-reset]").addEventListener("click", () => {
|
|
state = { remaining: duration, running: false, updatedAt: Date.now() };
|
|
saveState();
|
|
clearInterval(intervalId);
|
|
timer.classList.remove("timer--complete");
|
|
announcement.textContent = "Timer reset.";
|
|
render();
|
|
});
|
|
|
|
if (state.running) intervalId = window.setInterval(render, 250);
|
|
render();
|
|
}
|
|
|
|
document.querySelectorAll("[data-focus-timer]").forEach(initializeTimer);
|
|
|
|
const celebration = document.querySelector('[data-celebrate="true"]');
|
|
if (celebration && celebration.dataset.chime === "true") {
|
|
playCompletionChime();
|
|
}
|