65 lines
1.7 KiB
JavaScript
65 lines
1.7 KiB
JavaScript
|
|
// 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'),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|