Allow stopwatch and resuming runs, low resolution on saves, to not write to disk too often...

main 1.5.0
isark 1 year ago
parent 54fd9ca62e
commit 32a2424783

@ -47,6 +47,9 @@ pub struct Config {
pub num_visible: u16, pub num_visible: u16,
#[serde(default = "Config::default_plan_offset")] #[serde(default = "Config::default_plan_offset")]
pub offset: i16, pub offset: i16,
#[serde(default = "Config::default_enable_stopwatch")]
pub enable_stopwatch: bool,
} }
impl Default for Config { impl Default for Config {
@ -66,6 +69,7 @@ impl Default for Config {
note_default_fg: Self::default_note_default_fg(), note_default_fg: Self::default_note_default_fg(),
num_visible: Self::default_plan_num_visible(), num_visible: Self::default_plan_num_visible(),
offset: Self::default_plan_offset(), offset: Self::default_plan_offset(),
enable_stopwatch: Self::default_enable_stopwatch(),
} }
} }
} }
@ -118,4 +122,7 @@ impl Config {
fn default_plan_offset() -> i16 { fn default_plan_offset() -> i16 {
1 1
} }
fn default_enable_stopwatch() -> bool {
false
}
} }

@ -104,7 +104,6 @@ fn save_plan_at_path(path: PathBuf, plan: Plan) -> bool {
#[tauri::command] #[tauri::command]
fn save_plan_at_store(name: String, plan: Plan, allow_overwrite: bool) -> Option<PathBuf> { fn save_plan_at_store(name: String, plan: Plan, allow_overwrite: bool) -> Option<PathBuf> {
log::trace!("plan etag {:?}", plan.metadata.latest_server_etag);
Storage::save_plan_at_store_path(&name, plan, allow_overwrite).ok() Storage::save_plan_at_store_path(&name, plan, allow_overwrite).ok()
} }

@ -7,15 +7,16 @@ pub struct Plan {
plan: Vec<PlanElement>, plan: Vec<PlanElement>,
current: usize, current: usize,
#[serde(flatten)] #[serde(flatten)]
pub metadata: PlanMetadata, metadata: PlanMetadata,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct PlanMetadata { pub struct PlanMetadata {
stored_path: Option<PathBuf>, stored_path: Option<PathBuf>,
update_url: Option<String>, update_url: Option<String>,
pub latest_server_etag: Option<String>, latest_server_etag: Option<String>,
identifier: Option<String>, identifier: Option<String>,
last_stored_time: Option<u64>,
} }
impl PlanMetadata { impl PlanMetadata {
@ -29,12 +30,13 @@ impl Serialize for PlanMetadata {
where where
S: serde::Serializer, 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("update_url", &self.update_url)?;
state.serialize_field("stored_path", &self.stored_path)?; state.serialize_field("stored_path", &self.stored_path)?;
state.serialize_field("latest_server_etag", &self.latest_server_etag)?; state.serialize_field("latest_server_etag", &self.latest_server_etag)?;
state.serialize_field("identifier", &self.identifier)?; 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(path) = &self.stored_path {
if let Some(name) = path.file_name() { if let Some(name) = path.file_name() {
@ -133,6 +135,7 @@ pub fn convert_old(path: PathBuf) -> Option<Plan> {
update_url: None, update_url: None,
latest_server_etag: None, latest_server_etag: None,
identifier: None, identifier: None,
last_stored_time: None,
}, },
}) })
} }

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Nothing", "productName": "Nothing",
"version": "1.4.0" "version": "1.5.0"
}, },
"tauri": { "tauri": {
"systemTray": { "systemTray": {

@ -9,6 +9,7 @@ export interface PlanInterface {
name?: string; name?: string;
latest_server_etag?: string; latest_server_etag?: string;
identifier?: string, identifier?: string,
last_stored_time?: number;
} }
export interface PlanMetadata { export interface PlanMetadata {
@ -17,6 +18,7 @@ export interface PlanMetadata {
name: string; name: string;
latest_server_etag?: string; latest_server_etag?: string;
identifier?: string; identifier?: string;
last_stored_time?: number;
} }
export class Plan { export class Plan {
@ -26,6 +28,8 @@ export class Plan {
name?: string; name?: string;
latest_server_etag?: string; latest_server_etag?: string;
identifier?: string; identifier?: string;
last_stored_time?: number
private path?: string; private path?: string;
private selfSaveSubject: Subject<void> = new Subject<void>(); private selfSaveSubject: Subject<void> = new Subject<void>();
@ -41,6 +45,7 @@ export class Plan {
this.name = plan.name; this.name = plan.name;
this.latest_server_etag = plan.latest_server_etag; this.latest_server_etag = plan.latest_server_etag;
this.identifier = plan.identifier; this.identifier = plan.identifier;
this.last_stored_time = plan.last_stored_time;
this.selfSaveSubject.pipe(debounceTime(500)).subscribe(() => this.directSelfSave()); this.selfSaveSubject.pipe(debounceTime(500)).subscribe(() => this.directSelfSave());
} }
@ -70,10 +75,11 @@ export class Plan {
update_url: this.update_url, update_url: this.update_url,
latest_server_etag: this.latest_server_etag, latest_server_etag: this.latest_server_etag,
identifier: this.identifier, identifier: this.identifier,
last_stored_time: this.last_stored_time,
}; };
} }
private requestSelfSave() { public requestSelfSave() {
if (this.path) { if (this.path) {
this.selfSaveSubject.next(); this.selfSaveSubject.next();
} }

@ -46,6 +46,7 @@
zones</span> zones</span>
</div> </div>
</tooltip> </tooltip>
<span *ngIf="shouldDisplayTimer()" class="timer">{{currentTime()}}</span>
</div> </div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable" <ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"

@ -117,6 +117,12 @@ notes {
left: 0; left: 0;
} }
.timer {
position: absolute;
top: 0;
left: 32px;
}
.help-area { .help-area {
background-color: rgba(50, 50, 50, 1); background-color: rgba(50, 50, 50, 1);
border: 1px solid black; border: 1px solid black;
@ -136,7 +142,6 @@ notes {
font-size: 10rem; font-size: 10rem;
& span { & span {
display: flex; display: flex;
align-items: center; align-items: center;
@ -168,6 +173,8 @@ notes {
display: block; display: block;
overflow: hidden; overflow: hidden;
flex-grow: 1; flex-grow: 1;
background-color: rgba(200, 200, 220, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
mat-list { mat-list {
max-height: 100%; max-height: 100%;

@ -9,12 +9,20 @@ import { PlanService, UrlError } from '../_services/plan.service';
import { Plan, PlanElement, PlanMetadata } from '../_models/plan'; import { Plan, PlanElement, PlanMetadata } from '../_models/plan';
import { WorldAreaService } from '../_services/world-area.service'; import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area'; import { WorldArea } from '../_models/world-area';
import { Subscription, from } from 'rxjs'; import { Subscription, from, timer } from 'rxjs';
import { open } from '@tauri-apps/api/dialog'; import { open } from '@tauri-apps/api/dialog';
import { OverlayService, StateEvent } from '../_services/overlay.service'; import { OverlayService, StateEvent } from '../_services/overlay.service';
import { appWindow } from '@tauri-apps/api/window'; import { appWindow } from '@tauri-apps/api/window';
import { EventsService } from '../_services/events.service'; import { EventsService } from '../_services/events.service';
import { Event } from '@tauri-apps/api/event'; import { Event } from '@tauri-apps/api/event';
import { MatDialog } from '@angular/material/dialog';
import { ResumeDialog } from './resume-dialog.component';
enum Resume {
Discard,
Next,
Instant
}
@Component({ @Component({
selector: 'plan-display', selector: 'plan-display',
@ -45,9 +53,15 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
previousPlans: PlanMetadata[] = []; previousPlans: PlanMetadata[] = [];
checkingPlanUpdate: boolean = false; checkingPlanUpdate: boolean = false;
start?: Date;
latest?: Date;
timerSubscription?: Subscription;
debouncedSaveStopwatch?: Subscription;
recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>(); recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>();
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", () => { window.addEventListener("resize", () => {
this.zone.run(() => { this.zone.run(() => {
this.windowInitHandler() this.windowInitHandler()
@ -60,15 +74,26 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
}) })
this.planService.getCurrentPlan().subscribe(plan => { 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.currentPlan = plan;
this.start = undefined;
//Close settings anytime we get a new current plan. //Close settings anytime we get a new current plan.
this.settingsOpen = false; this.settingsOpen = false;
if (this.currentPlan.last_stored_time) {
this.askResume();
}
setTimeout(() => this.setIndex(plan.current), 0); setTimeout(() => this.setIndex(plan.current), 0);
}) })
this.registerOnZoneEnter(); this.registerOnZoneEnter();
} }
get disablePlans(): boolean { get disablePlans(): boolean {
return this.checkingPlanUpdate; return this.checkingPlanUpdate;
} }
@ -82,9 +107,41 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
if (entered.payload === this.currentPlan.plan[current + 1].area_key) { if (entered.payload === this.currentPlan.plan[current + 1].area_key) {
this.zone.run(() => this.next()); 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() { windowInitHandler() {
@ -364,6 +421,48 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating"; if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating";
return ""; 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 { function commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number {

@ -16,6 +16,8 @@ import {MatTooltipModule} from '@angular/material/tooltip';
import { TooltipComponent } from '../tooltip/tooltip.component'; import { TooltipComponent } from '../tooltip/tooltip.component';
import { AngularSvgIconModule } from 'angular-svg-icon'; import { AngularSvgIconModule } from 'angular-svg-icon';
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatDialogModule } from '@angular/material/dialog';
import { ResumeDialog } from './resume-dialog.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
PlanDisplayComponent PlanDisplayComponent
@ -37,7 +39,10 @@ import { ScrollingModule } from '@angular/cdk/scrolling';
MatTooltipModule, MatTooltipModule,
TooltipComponent, TooltipComponent,
AngularSvgIconModule, AngularSvgIconModule,
ScrollingModule ScrollingModule,
MatDialogModule,
ResumeDialog
], ],
exports: [ exports: [
PlanDisplayComponent PlanDisplayComponent

@ -0,0 +1,6 @@
Resume instantaneously, on next zone enter or not at all?
<div mat-dialog-actions>
<button mat-button color="color-resume-instant" (click)="instant()">Instant</button>
<button mat-button color="color-resume-next" (click)="next()" cdkFocusInitial>Next zone</button>
<button mat-button color="color-resume-no" (click)="discard()" cdkFocusInitial>Not at all</button>
</div>

@ -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<ResumeDialog>) {}
}

@ -59,4 +59,10 @@
[max]="configService.config.numVisible - 1" step="1"> [max]="configService.config.numVisible - 1" step="1">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable stopwatch</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.enableStopwatch"></mat-slide-toggle>
</div>
</div> </div>

@ -81,3 +81,15 @@ div.picker_wrapper.popup {
.mdc-notched-outline__notch { .mdc-notched-outline__notch {
clip-path: none !important; 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);
}
Loading…
Cancel
Save