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.
382 lines
11 KiB
382 lines
11 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",
|
|
CheckpointReached = "CheckpointReached",
|
|
}
|
|
|
|
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;
|
|
|
|
plan?: Plan;
|
|
|
|
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);
|
|
|
|
private pushSubject: Subject<TrackEntry> = new ReplaySubject<TrackEntry>(1);
|
|
private newCurrentRunHistorySubject: Subject<RunHistory> = new ReplaySubject<RunHistory>(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);
|
|
});
|
|
|
|
this.pushSubject.subscribe((entry) => {
|
|
this.currentRunHistory?.entries.push(entry);
|
|
});
|
|
}
|
|
|
|
public getCurrentRunHistory(): Observable<RunHistory> {
|
|
return this.newCurrentRunHistorySubject.asObservable();
|
|
}
|
|
|
|
getLatestEntry() {
|
|
return this.pushSubject.asObservable();
|
|
}
|
|
|
|
private setCurrentRunHistory(history: RunHistory) {
|
|
this.currentRunHistory = history;
|
|
this.newCurrentRunHistorySubject.next(history);
|
|
}
|
|
|
|
get elapsedTimeMillis() {
|
|
this.latest = new Date();
|
|
return this.latest!.valueOf() - this.start!.valueOf();
|
|
}
|
|
|
|
get isActive() {
|
|
return this.active;
|
|
}
|
|
|
|
get hasRunLoaded() {
|
|
return !!this.currentRunHistory;
|
|
}
|
|
|
|
public get storedHistories(): Observable<Map<string, RunHistoryMetadata>> {
|
|
return this.storedHistoriesSubject;
|
|
}
|
|
|
|
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.setCurrentRunHistory(history);
|
|
} else {
|
|
//Legacy or missing history, attempt to preserve elapsed time
|
|
this.setCurrentRunHistory(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.currentRunHistory!.plan = plan;
|
|
this.askResume(plan);
|
|
});
|
|
} else {
|
|
this.setCurrentRunHistory(this.createNew(plan.name));
|
|
this.currentRunHistory!.plan = plan;
|
|
this.resumeOnNext = true;
|
|
}
|
|
}
|
|
|
|
///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(() => { });
|
|
this.loadCache();
|
|
|
|
|
|
if (this.currentRunHistory.plan) {
|
|
this.currentRunHistory.plan.last_stored_time = this.currentRunHistory.uuid;
|
|
this.currentRunHistory.plan.requestSelfSave();
|
|
}
|
|
}
|
|
}
|
|
|
|
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.pushSubject.next({
|
|
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.pushSubject.next({
|
|
type: EntryType.PlanForcePrev,
|
|
zone: forced_area,
|
|
current_elapsed_millis: this.elapsedTimeMillis
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
//Not perfect but good enough..
|
|
public reportCheckpoint(checkpoint: string) {
|
|
this.pushSubject.next({
|
|
type: EntryType.CheckpointReached,
|
|
zone: checkpoint,
|
|
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();
|
|
}
|
|
}
|
|
|
|
public hmsTimestamp(elapsed?: number): string {
|
|
return hmsTimestamp(elapsed);
|
|
}
|
|
|
|
public loadCache() {
|
|
from(invoke<Map<string, RunHistoryMetadata>>('load_cache')).subscribe(data => {
|
|
this.zone.run(() => {
|
|
const cache = new Map<string, RunHistoryMetadata>();
|
|
Object.values(data).forEach((value) => {
|
|
cache.set(value.uuid, {
|
|
uuid: value.uuid,
|
|
currentElapsedMillis: value.current_elapsed_millis,
|
|
associatedName: value.associated_name,
|
|
last_updated: value.last_updated,
|
|
});
|
|
});
|
|
console.log("sending new cache!");
|
|
this.storedHistoriesSubject.next(cache);
|
|
});
|
|
});
|
|
}
|
|
|
|
private onZoneEnter(zone: string) {
|
|
if (!this.currentRunHistory) return;
|
|
|
|
if (this.configService.config.enableStopwatch) {
|
|
if(!this.isActive && this.resumeOnNext) {
|
|
this.resumeOnNext = false;
|
|
this.startStopwatch();
|
|
}
|
|
|
|
if(!this.isActive && !this.resumeOnNext) {
|
|
//Don't start timer if not meant to auto resumes on next.
|
|
return;
|
|
}
|
|
|
|
this.pushSubject.next({
|
|
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();
|
|
this.loadReachedCheckpoints();
|
|
break;
|
|
case Resume.Next:
|
|
this.resumeOnNext = true;
|
|
this.loadReachedCheckpoints();
|
|
break;
|
|
case Resume.Discard:
|
|
this.setCurrentRunHistory(this.createNew(plan.name));
|
|
this.loadReachedCheckpoints();
|
|
plan.last_stored_time = this.currentRunHistory!.uuid;
|
|
this.resumeOnNext = true;
|
|
plan.requestSelfSave();
|
|
break;
|
|
}
|
|
})
|
|
}
|
|
|
|
loadReachedCheckpoints() {
|
|
if (!this.currentRunHistory || !this.currentRunHistory.plan || !this.configService.config.runCompareHistory) return;
|
|
|
|
this.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
|
|
if (!history) return;
|
|
const checkpoints = new Map(history.entries.filter(entry => entry.type === EntryType.CheckpointReached).map(entry => [entry.zone, entry.current_elapsed_millis]));
|
|
const ourCheckpointValidZones = new Map(this.currentRunHistory?.plan?.plan.filter(entry => checkpoints.has(entry.uuid!)).map(entry => [entry.uuid!, entry]));
|
|
this.currentRunHistory?.entries.filter(entry => entry.type === EntryType.CheckpointReached).forEach(entry => {
|
|
if (ourCheckpointValidZones.has(entry.zone)) {
|
|
ourCheckpointValidZones.get(entry.zone)!.checkpoint_your_millis = entry.current_elapsed_millis;
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
private createNew(associatedName?: string) {
|
|
const uuid = uuidv4();
|
|
|
|
return new RunHistory({
|
|
uuid,
|
|
associated_name: associatedName || 'Unnamed',
|
|
current_elapsed_millis: 0,
|
|
last_updated: Date.now(),
|
|
entries: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
export function hmsTimestamp(elapsed?: number): string {
|
|
if (elapsed == null || elapsed == undefined) return "N/A";
|
|
|
|
const h = String(Math.floor(elapsed / 3600000)).padStart(2, '0');
|
|
const m = String(Math.floor((elapsed % 3600000) / 60000)).padStart(2, '0');
|
|
const s = String(Math.floor((elapsed % 60000) / 1000)).padStart(2, '0');
|
|
|
|
return `${h}:${m}:${s}`;
|
|
} |