Added update for tabbar
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import {Component, computed, OnDestroy, signal} from '@angular/core';
|
||||
import {Component, computed, effect, inject, OnDestroy, signal} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
@@ -6,6 +6,7 @@ import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
|
||||
type StopwatchState = 'idle' | 'running' | 'paused';
|
||||
@@ -37,22 +38,33 @@ export class StopwatchComponent implements OnDestroy {
|
||||
|
||||
readonly display = computed(() => formatElapsed(this.elapsedMs()));
|
||||
|
||||
private rafId: number | null = null;
|
||||
private intervalId: number | null = null;
|
||||
private startTimestamp = 0;
|
||||
private accumulatedMs = 0;
|
||||
private previousElapsedMs = 0;
|
||||
private audioContext: AudioContext | null = null;
|
||||
private flashTimeoutId: number | null = null;
|
||||
private readonly titleService = inject(Title);
|
||||
private readonly originalTitle = this.titleService.getTitle();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const state = this.state();
|
||||
const elapsed = this.elapsedMs();
|
||||
this.updateDocumentTitle(state, elapsed);
|
||||
});
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.state() === 'running') {
|
||||
return;
|
||||
}
|
||||
this.ensureAudioContext();
|
||||
this.resumeAudioContext();
|
||||
this.startTimestamp = performance.now();
|
||||
this.previousElapsedMs = this.accumulatedMs;
|
||||
this.state.set('running');
|
||||
this.tick();
|
||||
this.startTicking();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
@@ -86,6 +98,14 @@ export class StopwatchComponent implements OnDestroy {
|
||||
this.audioContext.close().catch(() => undefined);
|
||||
this.audioContext = null;
|
||||
}
|
||||
this.titleService.setTitle(this.originalTitle);
|
||||
}
|
||||
|
||||
private startTicking(): void {
|
||||
this.cancelTick();
|
||||
this.tick();
|
||||
// setInterval keeps firing in background tabs (throttled to ~1s) so beeps still play when unfocused.
|
||||
this.intervalId = window.setInterval(this.tick, 50);
|
||||
}
|
||||
|
||||
private tick = (): void => {
|
||||
@@ -98,17 +118,24 @@ export class StopwatchComponent implements OnDestroy {
|
||||
this.checkIntervalCrossing(this.previousElapsedMs, newElapsed);
|
||||
this.previousElapsedMs = newElapsed;
|
||||
this.elapsedMs.set(newElapsed);
|
||||
|
||||
this.rafId = requestAnimationFrame(this.tick);
|
||||
};
|
||||
|
||||
private cancelTick(): void {
|
||||
if (this.rafId !== null) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
if (this.intervalId !== null) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private updateDocumentTitle(state: StopwatchState, elapsed: number): void {
|
||||
if (state === 'idle') {
|
||||
this.titleService.setTitle(this.originalTitle);
|
||||
return;
|
||||
}
|
||||
const prefix = state === 'running' ? '▶' : '⏸';
|
||||
this.titleService.setTitle(`${prefix} ${formatElapsedShort(elapsed)} · ${this.originalTitle}`);
|
||||
}
|
||||
|
||||
private checkIntervalCrossing(prevMs: number, currMs: number): void {
|
||||
if (!this.intervalEnabled()) {
|
||||
return;
|
||||
@@ -146,6 +173,13 @@ export class StopwatchComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private resumeAudioContext(): void {
|
||||
// Browsers suspend AudioContext until a user gesture; resume on start so beeps can play in background tabs.
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private beep(): void {
|
||||
if (!this.audioContext) {
|
||||
return;
|
||||
@@ -181,6 +215,19 @@ function formatElapsed(totalMs: number): string {
|
||||
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}.${pad(ms, 3)}`;
|
||||
}
|
||||
|
||||
function formatElapsedShort(totalMs: number): string {
|
||||
const totalSeconds = Math.floor(totalMs / 1000);
|
||||
const seconds = totalSeconds % 60;
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(seconds, 2)}`;
|
||||
}
|
||||
return `${pad(minutes, 2)}:${pad(seconds, 2)}`;
|
||||
}
|
||||
|
||||
function pad(value: number, length: number): string {
|
||||
return value.toString().padStart(length, '0');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user