diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index d4f8744..c0909f8 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -47,6 +47,9 @@ pub struct Config { pub num_visible: u16, #[serde(default = "Config::default_plan_offset")] pub offset: i16, + + #[serde(default = "Config::default_enable_stopwatch")] + pub enable_stopwatch: bool, } impl Default for Config { @@ -66,6 +69,7 @@ impl Default for Config { note_default_fg: Self::default_note_default_fg(), num_visible: Self::default_plan_num_visible(), offset: Self::default_plan_offset(), + enable_stopwatch: Self::default_enable_stopwatch(), } } } @@ -118,4 +122,7 @@ impl Config { fn default_plan_offset() -> i16 { 1 } + fn default_enable_stopwatch() -> bool { + false + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 52cdd41..13cd01f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -104,7 +104,6 @@ fn save_plan_at_path(path: PathBuf, plan: Plan) -> bool { #[tauri::command] fn save_plan_at_store(name: String, plan: Plan, allow_overwrite: bool) -> Option { - log::trace!("plan etag {:?}", plan.metadata.latest_server_etag); Storage::save_plan_at_store_path(&name, plan, allow_overwrite).ok() } diff --git a/src-tauri/src/plan.rs b/src-tauri/src/plan.rs index 858adac..59ddafa 100644 --- a/src-tauri/src/plan.rs +++ b/src-tauri/src/plan.rs @@ -7,15 +7,16 @@ pub struct Plan { plan: Vec, current: usize, #[serde(flatten)] - pub metadata: PlanMetadata, + metadata: PlanMetadata, } #[derive(Deserialize, Debug, Clone)] pub struct PlanMetadata { stored_path: Option, update_url: Option, - pub latest_server_etag: Option, + latest_server_etag: Option, identifier: Option, + last_stored_time: Option, } impl PlanMetadata { @@ -29,12 +30,13 @@ impl Serialize for PlanMetadata { where S: serde::Serializer, { - let mut state = serializer.serialize_struct("PlanMetadata", 5)?; + let mut state = serializer.serialize_struct("PlanMetadata", 6)?; state.serialize_field("update_url", &self.update_url)?; state.serialize_field("stored_path", &self.stored_path)?; state.serialize_field("latest_server_etag", &self.latest_server_etag)?; state.serialize_field("identifier", &self.identifier)?; + state.serialize_field("last_stored_time", &self.last_stored_time)?; if let Some(path) = &self.stored_path { if let Some(name) = path.file_name() { @@ -133,6 +135,7 @@ pub fn convert_old(path: PathBuf) -> Option { update_url: None, latest_server_etag: None, identifier: None, + last_stored_time: None, }, }) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c98f51a..423192d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Nothing", - "version": "1.4.0" + "version": "1.5.0" }, "tauri": { "systemTray": { diff --git a/src/app/_models/plan.ts b/src/app/_models/plan.ts index 33e38fa..b006065 100644 --- a/src/app/_models/plan.ts +++ b/src/app/_models/plan.ts @@ -9,6 +9,7 @@ export interface PlanInterface { name?: string; latest_server_etag?: string; identifier?: string, + last_stored_time?: number; } export interface PlanMetadata { @@ -17,6 +18,7 @@ export interface PlanMetadata { name: string; latest_server_etag?: string; identifier?: string; + last_stored_time?: number; } export class Plan { @@ -26,6 +28,8 @@ export class Plan { name?: string; latest_server_etag?: string; identifier?: string; + last_stored_time?: number + private path?: string; private selfSaveSubject: Subject = new Subject(); @@ -41,6 +45,7 @@ export class Plan { this.name = plan.name; this.latest_server_etag = plan.latest_server_etag; this.identifier = plan.identifier; + this.last_stored_time = plan.last_stored_time; this.selfSaveSubject.pipe(debounceTime(500)).subscribe(() => this.directSelfSave()); } @@ -70,10 +75,11 @@ export class Plan { update_url: this.update_url, latest_server_etag: this.latest_server_etag, identifier: this.identifier, + last_stored_time: this.last_stored_time, }; } - private requestSelfSave() { + public requestSelfSave() { if (this.path) { this.selfSaveSubject.next(); } diff --git a/src/app/plan-display/plan-display.component.html b/src/app/plan-display/plan-display.component.html index ab43c59..32ccfa5 100644 --- a/src/app/plan-display/plan-display.component.html +++ b/src/app/plan-display/plan-display.component.html @@ -46,6 +46,7 @@ zones + {{currentTime()}} = new Map(); + resumeOnNext: boolean = false; - constructor(private events: EventsService, public configService: ConfigService, private cdr: ChangeDetectorRef, private shortcut: ShortcutService, public planService: PlanService, public worldAreaService: WorldAreaService, public overlayService: OverlayService, private zone: NgZone) { + constructor(private events: EventsService, public configService: ConfigService, private cdr: ChangeDetectorRef, private shortcut: ShortcutService, public planService: PlanService, public worldAreaService: WorldAreaService, public overlayService: OverlayService, private zone: NgZone, public dialog: MatDialog) { window.addEventListener("resize", () => { this.zone.run(() => { this.windowInitHandler() @@ -60,15 +74,26 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { }) this.planService.getCurrentPlan().subscribe(plan => { + if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe(); + if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe(); + this.currentPlan = plan; + + this.start = undefined; //Close settings anytime we get a new current plan. this.settingsOpen = false; + + if (this.currentPlan.last_stored_time) { + this.askResume(); + } + setTimeout(() => this.setIndex(plan.current), 0); }) this.registerOnZoneEnter(); } + get disablePlans(): boolean { return this.checkingPlanUpdate; } @@ -82,11 +107,43 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { if (entered.payload === this.currentPlan.plan[current + 1].area_key) { this.zone.run(() => this.next()); } + + console.log("config enableStopwatch", this.configService.config.enableStopwatch) + if (this.configService.config.enableStopwatch) { + console.log("this.start", this.start); + if (entered.payload === this.currentPlan!.plan[0].area_key && !this.start || this.resumeOnNext) { + this.resumeOnNext = false; + this.startStopwatch(); + } + } } } }); } + ///Assumes currentPlan is set... + private startStopwatch() { + console.log("Starting stopwatch"); + if (this.currentPlan?.last_stored_time) { + this.start = new Date(Date.now() - this.currentPlan.last_stored_time); + } else { + this.start = new Date(); + } + + if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe(); + if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe(); + + this.timerSubscription = timer(0, 1000).subscribe(() => { + this.zone.run(() => { this.latest = new Date(); }); + }); + + this.debouncedSaveStopwatch = timer(0, 10000).subscribe(() => { + this.currentPlan!.last_stored_time = this.elapsedTimeMillis(); + console.log("last stored time at save attempt", this.currentPlan!.last_stored_time); + this.currentPlan!.requestSelfSave(); + }) + } + windowInitHandler() { if (window.innerWidth > 0) { this.ngAfterViewInit(); @@ -364,6 +421,48 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating"; return ""; } + + currentTime() { + if (!this.latest || !this.start) return ""; + + const diff = this.latest.valueOf() - this.start.valueOf(); + const h = String(Math.floor(diff / 3600000)).padStart(2, '0'); + const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0'); + const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0'); + + return `${h}:${m}:${s}`; + } + + elapsedTimeMillis() { + return this.latest!.valueOf() - this.start!.valueOf(); + } + + shouldDisplayTimer(): boolean { + if (!this.configService.config.enableStopwatch) return false; + if (!this.start || !this.latest) return false; + + return this.overlayService.visible && !this.overlayService.interactable; + } + + askResume() { + console.log("Asking resume"); + 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.currentPlan!.last_stored_time = undefined; + this.currentPlan!.requestSelfSave(); + break; + } + }) + } } function commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number { diff --git a/src/app/plan-display/plan-display.module.ts b/src/app/plan-display/plan-display.module.ts index 66dd1c3..cb7cae7 100644 --- a/src/app/plan-display/plan-display.module.ts +++ b/src/app/plan-display/plan-display.module.ts @@ -16,6 +16,8 @@ import {MatTooltipModule} from '@angular/material/tooltip'; import { TooltipComponent } from '../tooltip/tooltip.component'; import { AngularSvgIconModule } from 'angular-svg-icon'; import { ScrollingModule } from '@angular/cdk/scrolling'; +import { MatDialogModule } from '@angular/material/dialog'; +import { ResumeDialog } from './resume-dialog.component'; @NgModule({ declarations: [ PlanDisplayComponent @@ -37,7 +39,10 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; MatTooltipModule, TooltipComponent, AngularSvgIconModule, - ScrollingModule + ScrollingModule, + MatDialogModule, + ResumeDialog + ], exports: [ PlanDisplayComponent diff --git a/src/app/plan-display/resume-dialog.component.html b/src/app/plan-display/resume-dialog.component.html new file mode 100644 index 0000000..0223990 --- /dev/null +++ b/src/app/plan-display/resume-dialog.component.html @@ -0,0 +1,6 @@ +Resume instantaneously, on next zone enter or not at all? +
+ + + +
\ No newline at end of file diff --git a/src/app/plan-display/resume-dialog.component.ts b/src/app/plan-display/resume-dialog.component.ts new file mode 100644 index 0000000..4ae4c38 --- /dev/null +++ b/src/app/plan-display/resume-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { MatButtonModule } from "@angular/material/button"; +import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; + +export enum Resume { + Discard, + Next, + Instant +} + +@Component({ + selector: 'resume-dialog', + templateUrl: 'resume-dialog.component.html', + standalone: true, + imports: [MatDialogModule, MatFormFieldModule, MatInputModule, FormsModule, MatButtonModule], +}) +export class ResumeDialog { + + instant() { + this.dialogRef.close(Resume.Instant); + } + next() { + this.dialogRef.close(Resume.Next); + } + discard() { + this.dialogRef.close(Resume.Discard); + } + + constructor(public dialogRef: MatDialogRef) {} +} \ No newline at end of file diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 755f5d7..04aace6 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -59,4 +59,10 @@ [max]="configService.config.numVisible - 1" step="1"> + +
+ Enable stopwatch + +
\ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index c7f960c..006c574 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -80,4 +80,16 @@ div.picker_wrapper.popup { .mdc-notched-outline__notch { clip-path: none !important; +} + +.mat-color-resume-instant { + background-color: rgb(76, 146, 146); +} + +.mat-color-resume-next { + background-color: rgb(120, 76, 146); +} + +.mat-color-resume-no { + background-color: rgb(180, 41, 41); } \ No newline at end of file