2026-10-10 06:45:15 +00:00
|
|
|
// BigInt keeps entered integers exact, even for very large countdowns.
|
|
|
|
|
export function integer(value) {
|
|
|
|
|
return /^\d+$/.test(String(value)) ? BigInt(value) : 0n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function splitSeconds(seconds) {
|
|
|
|
|
return {
|
|
|
|
|
minutes: String(seconds / 60n),
|
|
|
|
|
seconds: String(seconds % 60n).padStart(2, '0'),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 06:50:45 +00:00
|
|
|
export function adjustTime(minutes, seconds, field, delta) {
|
|
|
|
|
const total = integer(minutes) * 60n + integer(seconds);
|
|
|
|
|
if (field === 'minutes') {
|
|
|
|
|
const adjusted = total / 60n + delta;
|
|
|
|
|
return splitSeconds((adjusted < 0n ? 0n : adjusted) * 60n + total % 60n);
|
|
|
|
|
}
|
|
|
|
|
const adjusted = total + delta;
|
|
|
|
|
return splitSeconds(adjusted < 0n ? 0n : adjusted);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 06:45:15 +00:00
|
|
|
export class Countdown {
|
|
|
|
|
constructor(now = Date.now) {
|
|
|
|
|
this.now = now;
|
|
|
|
|
this.state = 'idle';
|
|
|
|
|
this.finished = false;
|
|
|
|
|
this.duration = 0n;
|
|
|
|
|
this.remaining = 0n;
|
|
|
|
|
this.deadline = 0n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
edit(minutes, seconds) {
|
|
|
|
|
if (this.state === 'running') return;
|
|
|
|
|
this.duration = (integer(minutes) * 60n + integer(seconds)) * 1000n;
|
|
|
|
|
this.remaining = this.duration;
|
|
|
|
|
this.state = 'idle';
|
|
|
|
|
this.finished = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update() {
|
|
|
|
|
if (this.state !== 'running') return;
|
|
|
|
|
const delta = this.deadline - BigInt(this.now());
|
|
|
|
|
this.remaining = delta > 0n ? delta : 0n;
|
|
|
|
|
if (this.remaining === 0n) {
|
|
|
|
|
this.state = 'idle';
|
|
|
|
|
this.finished = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggle() {
|
|
|
|
|
if (this.state === 'running') {
|
|
|
|
|
this.update();
|
|
|
|
|
if (!this.finished) this.state = 'paused';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.finished) this.remaining = this.duration;
|
|
|
|
|
this.finished = false;
|
|
|
|
|
this.deadline = BigInt(this.now()) + this.remaining;
|
|
|
|
|
this.state = 'running';
|
|
|
|
|
this.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset() {
|
|
|
|
|
this.remaining = this.duration;
|
|
|
|
|
this.state = 'idle';
|
|
|
|
|
this.finished = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display() {
|
|
|
|
|
// Round up so the timer only displays zero once its deadline is reached.
|
|
|
|
|
return splitSeconds((this.remaining + 999n) / 1000n);
|
|
|
|
|
}
|
|
|
|
|
}
|