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.
281 lines
7.7 KiB
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: [],
|
|
});
|
|
}
|
|
} |