1+ import { isNumber } from './utils' ;
2+ import { LocalStorageVault } from './vault' ;
3+
4+ export default class ForegroundTimeTracker {
5+ private isTrackerActive : boolean = false ;
6+ private localStorageName : string = '' ;
7+ private timerVault : LocalStorageVault < number > ;
8+ public startTime : number = 0 ;
9+ public totalTime : number = 0 ;
10+
11+ constructor ( timerKey : string ) {
12+ this . localStorageName = `mp-time-${ timerKey } ` ;
13+ this . timerVault = new LocalStorageVault < number > ( this . localStorageName ) ;
14+ this . loadTimeFromStorage ( ) ;
15+ this . addHandlers ( ) ;
16+ if ( document . hidden === false ) {
17+ this . startTracking ( ) ;
18+ }
19+ }
20+
21+ private addHandlers ( ) : void {
22+ // when user switches tabs or minimizes the window
23+ document . addEventListener ( 'visibilitychange' , ( ) =>
24+ this . handleVisibilityChange ( )
25+ ) ;
26+ // when user switches to another application
27+ window . addEventListener ( 'blur' , ( ) => this . handleWindowBlur ( ) ) ;
28+ // when window gains focus
29+ window . addEventListener ( 'focus' , ( ) => this . handleWindowFocus ( ) ) ;
30+ // this ensures that timers between tabs are in sync
31+ window . addEventListener ( 'storage' , event => this . syncAcrossTabs ( event ) ) ;
32+ // when user closes tab, refreshes, or navigates to another page via link
33+ window . addEventListener ( 'beforeunload' , ( ) =>
34+ this . updateTimeInPersistence ( )
35+ ) ;
36+ }
37+
38+ private handleVisibilityChange ( ) : void {
39+ if ( document . hidden ) {
40+ this . stopTracking ( ) ;
41+ } else {
42+ this . startTracking ( ) ;
43+ }
44+ }
45+
46+ private handleWindowBlur ( ) : void {
47+ if ( this . isTrackerActive ) {
48+ this . stopTracking ( ) ;
49+ }
50+ }
51+
52+ private handleWindowFocus ( ) : void {
53+ if ( ! this . isTrackerActive ) {
54+ this . startTracking ( ) ;
55+ }
56+ }
57+
58+ private syncAcrossTabs ( event : StorageEvent ) : void {
59+ if ( event . key === this . localStorageName && event . newValue !== null ) {
60+ const newTime = parseFloat ( event . newValue ) || 0 ;
61+ this . totalTime = newTime ;
62+ }
63+ }
64+
65+ public updateTimeInPersistence ( ) : void {
66+ if ( this . isTrackerActive ) {
67+ this . timerVault . store ( Math . round ( this . totalTime ) ) ;
68+ }
69+ }
70+
71+ private loadTimeFromStorage ( ) : void {
72+ const storedTime = this . timerVault . retrieve ( ) ;
73+ if ( isNumber ( storedTime ) && storedTime !== null ) {
74+ this . totalTime = storedTime ;
75+ }
76+ }
77+
78+
79+ private startTracking ( ) : void {
80+ if ( ! document . hidden ) {
81+ this . startTime = Math . floor ( performance . now ( ) ) ;
82+ this . isTrackerActive = true ;
83+ }
84+ }
85+
86+ private stopTracking ( ) : void {
87+ if ( this . isTrackerActive ) {
88+ this . setTotalTime ( ) ;
89+ this . updateTimeInPersistence ( ) ;
90+ this . isTrackerActive = false ;
91+ }
92+ }
93+
94+ private setTotalTime ( ) : void {
95+ if ( this . isTrackerActive ) {
96+ const now = Math . floor ( performance . now ( ) ) ;
97+ this . totalTime += now - this . startTime ;
98+ this . startTime = now ;
99+
100+ }
101+ }
102+
103+ public getTimeInForeground ( ) : number {
104+ this . setTotalTime ( ) ;
105+ this . updateTimeInPersistence ( ) ;
106+ return this . totalTime ;
107+ }
108+
109+ public resetTimer ( ) : void {
110+ this . totalTime = 0 ;
111+ this . updateTimeInPersistence ( ) ;
112+ }
113+ }
0 commit comments