You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Nothing/src/app/_services/time-tracker.service.ts

281 lines
7.7 KiB

import { Injectable, NgZone } from '@angular/core';
import { ConfigService } from './config.service';
import { Observable, ReplaySubject, Subject, Subscribable, Subscription, from, map, tap, timer } from 'rxjs';
import { Plan } from '../_models/plan';
import { MatDialog } from '@angular/material/dialog';
import { Resume, ResumeDialog } from '../plan-display/resume-dialog.component';
import { v4 as uuidv4 } from 'uuid';
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
export enum EntryType {
PlanForceNext = "PlanForceNext",
PlanForcePrev = "PlanForcePrev",
ZoneEnter = "ZoneEnter",
}
export interface TrackEntry {
type: EntryType;
zone: string;
current_elapsed_millis: number;
}
export interface RunHistoryMetadata {
uuid: string;
currentElapsedMillis: number;
associatedName: string;
last_updated: number;
}
interface RunHistoryInterface {
uuid: string;
current_elapsed_millis: number;
associated_name: string;
last_updated: number;
entries: TrackEntry[];
}
export class RunHistory {
uuid: string;
currentElapsedMillis: number;
entries: TrackEntry[];
associatedName: string;
last_updated: number;
constructor(data: RunHistoryInterface) {
this.uuid = data.uuid;
this.currentElapsedMillis = data.current_elapsed_millis;
this.entries = data.entries;
this.last_updated = data.last_updated;
this.associatedName = data.associated_name;
}
toInterface(): RunHistoryInterface {
return {
uuid: this.uuid,
current_elapsed_millis: this.currentElapsedMillis,
last_updated: this.last_updated,
entries: this.entries,
associated_name: this.associatedName,
}
}
}
@Injectable({
providedIn: 'root'
})
export class TimeTrackerService {
private currentRunHistory?: RunHistory;
private timerSubscription?: Subscription;
private debouncedSaveStopwatch?: Subscription;
resumeOnNext: boolean = false;
private start?: Date;
private latest?: Date;
private active: boolean = false;
private storedHistoriesSubject: Subject<Map<string, RunHistoryMetadata>> = new ReplaySubject<Map<string, RunHistoryMetadata>>(1);
constructor(private configService: ConfigService, public dialog: MatDialog, private zone: NgZone) {
this.loadCache();
appWindow.listen("entered", (entered) => {
if (entered.payload && typeof entered.payload === 'string')
this.onZoneEnter(entered.payload);
});
}
get elapsedTimeMillis() {
return this.latest!.valueOf() - this.start!.valueOf();
}
get isActive() {
return this.active;
}
get hasRunLoaded() {
return !!this.currentRunHistory;
}
onNewRun(plan: Plan) {
if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe();
if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe();
this.start = undefined;
this.latest = undefined;
this.active = false;
if (plan.last_stored_time) {
this.loadHistory(plan.last_stored_time).subscribe(history => {
if (history) {
this.currentRunHistory = history;
} else {
//Legacy or missing history, attempt to preserve elapsed time
this.currentRunHistory = this.createNew(plan.name);
plan.last_stored_time = this.currentRunHistory.uuid;
const old_time = parseInt(plan.last_stored_time, 10);
if (!isNaN(old_time) && old_time > 0) {
this.currentRunHistory.currentElapsedMillis = old_time;
}
plan.requestSelfSave();
}
this.askResume(plan);
});
}
}
///Assumes currentPlan is set...
private startStopwatch() {
this.stop(); // Make sure we stop before starting again
if (this.currentRunHistory?.currentElapsedMillis) {
this.start = new Date(Date.now() - this.currentRunHistory.currentElapsedMillis);
} else {
this.start = new Date();
}
this.latest = new Date();
this.active = true;
//Make sure this is always cleared if e.g. force started! should be fine but just in case!!
this.resumeOnNext = false;
this.timerSubscription = timer(0, 1000).subscribe(() => {
this.zone.run(() => {
this.latest = new Date();
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
});
});
this.debouncedSaveStopwatch = timer(0, 5000).subscribe(() => {
this.underlyingSaveStopwatch();
})
}
private underlyingSaveStopwatch() {
if(this.currentRunHistory && this.active && this.start && this.latest) {
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
this.currentRunHistory!.last_updated = Date.now();
this.saveHistory(this.currentRunHistory!).subscribe(() => { });
}
}
public saveHistory(currentRunHistory: RunHistory) {
return from(invoke('save_history', { currentRunHistory: currentRunHistory.toInterface() })).pipe(tap(() => this.loadCache()));
}
public loadHistory(uuid: string) {
return from(invoke<RunHistoryInterface>('load_history_at_uuid', { uuid: uuid })).pipe(map(history => {
if (history) {
return new RunHistory(history);
}
return undefined;
}));
}
public onForceNext(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.currentRunHistory?.entries.push({
type: EntryType.PlanForceNext,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
}
public onForcePrev(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.currentRunHistory?.entries.push({
type: EntryType.PlanForcePrev,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
}
public stop() {
if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe();
if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe();
//Do a nice little save here as well!
this.underlyingSaveStopwatch();
this.start = undefined;
this.latest = undefined;
this.active = false;
}
public startLoaded() {
if (this.currentRunHistory) {
this.startStopwatch();
}
}
private loadCache() {
from(invoke<Map<string, RunHistoryMetadata>>('load_cache')).subscribe(data => {
this.storedHistoriesSubject.next(data);
});
}
private onZoneEnter(zone: string) {
if (!this.currentRunHistory) return;
if (this.configService.config.enableStopwatch) {
if (!this.start || this.resumeOnNext) {
this.resumeOnNext = false;
this.startStopwatch();
}
this.currentRunHistory?.entries.push({
type: EntryType.ZoneEnter,
zone: zone,
current_elapsed_millis: this.elapsedTimeMillis
});
}
}
private askResume(plan: Plan) {
const dialogRef = this.dialog.open(ResumeDialog, { disableClose: true });
dialogRef.afterClosed().subscribe(resume => {
switch (resume) {
case Resume.Instant:
this.startStopwatch();
break;
case Resume.Next:
this.resumeOnNext = true;
break;
case Resume.Discard:
this.currentRunHistory = this.createNew(plan.name);
plan.last_stored_time = this.currentRunHistory.uuid;
plan.requestSelfSave();
break;
}
})
}
private createNew(associatedName?: string) {
const uuid = uuidv4();
return new RunHistory({
uuid,
associated_name: associatedName || 'Unnamed',
current_elapsed_millis: 0,
last_updated: Date.now(),
entries: [],
});
}
}