diff --git a/package.json b/package.json index 2c8ce33..900d950 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tauri-apps/api": "^1.2.0", "@types/markdown-it": "^13.0.0", "@types/natural-compare": "^1.4.1", + "@types/uuid": "^9.0.8", "angular-resize-event": "^3.2.0", "angular-svg-icon": "^17.0.0", "bootstrap": "^5.3.2", @@ -33,6 +34,7 @@ "ngx-moveable": "^0.48.1", "rxjs": "~7.8.1", "tslib": "^2.6.0", + "uuid": "^9.0.1", "vanilla-picker": "^2.12.1", "zone.js": "^0.13.1" }, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f7d0882..c58f100 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1972,6 +1972,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_with", "simple_logger", "statig", "steamlocate", @@ -2943,9 +2944,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.2.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1402f54f9a3b9e2efe71c1cea24e648acce55887983553eeb858cf3115acfd49" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ "base64 0.21.2", "chrono", @@ -2953,6 +2954,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.0.0", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -2960,9 +2962,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.2.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9197f1ad0e3c173a0222d3c4404fb04c3afe87e962bcb327af73e8301fa203c7" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ "darling", "proc-macro2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6be2bca..a16b712 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ notify = "6.0.1" regex = "1.9.3" lazy_static = "1.4.0" uuid = { version = "1.6.1", features = ["v4", "serde"] } +serde_with = "3.7.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f6917b4..ceb6d3e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::collections::HashMap; use std::sync::mpsc::Receiver; use std::{path::PathBuf, sync::Mutex}; @@ -25,12 +26,14 @@ use tauri::SystemTrayMenuItem; use tauri::Window; use lazy_static::lazy_static; +use time::{RunHistory, RunHistoryMetadata}; mod config; mod overlay; mod plan; mod poe_reader; mod storage; +mod time; lazy_static! { static ref WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map( @@ -72,6 +75,21 @@ fn enumerate_stored_plans() -> Vec { Storage::enumerate_plans() } +#[tauri::command] +fn save_history(current_run_history: RunHistory) { + Storage::save_history(current_run_history); +} + +#[tauri::command] +fn load_history_at_uuid(uuid: String) -> Option { + Storage::load_history_at_uuid(uuid) +} + +#[tauri::command] +fn load_cache() -> Option> { + Storage::load_cache() +} + #[tauri::command] fn load_plan_at_path( path: PathBuf, @@ -149,7 +167,7 @@ fn main() { .expect("Could not get main overlay window"), ); } - + // app.get_window("Overlay") // .expect("Could not get main overlay window") // .open_devtools(); @@ -166,6 +184,9 @@ fn main() { save_plan_at_path, save_plan_at_store, base_plan, + save_history, + load_history_at_uuid, + load_cache, ]) .system_tray(system_tray) .on_system_tray_event(|app, event| match event { @@ -206,9 +227,9 @@ fn listen_for_zone_changes(poe_client_log_path: Option, window: Window) let world_areas: WorldAreasMap = load_world_areas(); for area in blocking_area_filtered_rx(&enter_area_receiver) { if let Some(area) = filter_func(area) { - if let Some(entered) = world_areas.get(&area) { - window.emit_to("Overlay", "entered", &entered.named_id).ok(); - } + // if let Some(entered) = world_areas.get(&area) { + window.emit_to("Overlay", "entered", &area).ok(); + // } } } } diff --git a/src-tauri/src/plan.rs b/src-tauri/src/plan.rs index 59ddafa..41d99a4 100644 --- a/src-tauri/src/plan.rs +++ b/src-tauri/src/plan.rs @@ -1,6 +1,7 @@ use std::{collections::HashMap, path::PathBuf}; -use serde::{ser::SerializeStruct, Deserialize, Serialize}; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize}; +use serde_json::Value; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Plan { @@ -16,7 +17,9 @@ pub struct PlanMetadata { update_url: Option, latest_server_etag: Option, identifier: Option, - last_stored_time: Option, + #[serde(default)] + #[serde(deserialize_with = "deserialize_option_string")] + last_stored_time: Option, } impl PlanMetadata { @@ -48,6 +51,19 @@ impl Serialize for PlanMetadata { } } +// Custom deserializer that accepts both strings and legacy number for the stored_time field +fn deserialize_option_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let v: Option = Option::deserialize(deserializer)?; + Ok(match v { + Some(Value::String(s)) => Some(s), + Some(Value::Number(n)) => Some(n.to_string()), + _ => None, + }) +} + impl From for Option { fn from(metadata: PlanMetadata) -> Self { Some(serde_json::from_slice(&std::fs::read(metadata.stored_path?).ok()?).ok()?) diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index 37c380f..778f145 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::error::Error; use std::fs::DirEntry; use std::path::PathBuf; @@ -7,6 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::config::Config; use crate::plan::{convert_old, Plan, PlanMetadata}; +use crate::time::{RunHistory, RunHistoryMetadata}; #[derive(Serialize, Deserialize, Debug)] pub struct Storage { @@ -18,7 +20,9 @@ const QUALIFIER: &'static str = "me"; const ORGANIZATION: &'static str = "isark.poe"; const APPLICATION: &'static str = "Nothing"; const CONFIG_FILE: &'static str = "configuration.json"; +const HISTORY_CACHE_FILE: &'static str = "history_cache.json"; const SAVED_PLANS: &'static str = "plans"; +const SAVED_HISTORIES: &'static str = "histories"; fn mkdir_for_storage() { let dir_structure = Storage::plan_dir(); @@ -27,6 +31,13 @@ fn mkdir_for_storage() { .map_err(|_e| log::error!("Could not create directory for storing config and saves")) .ok(); } + + let dir_structure: Option = Storage::history_dir(); + if let Some(dir_structure) = dir_structure { + std::fs::create_dir_all(dir_structure) + .map_err(|_e| log::error!("Could not create directory for storing config and saves")) + .ok(); + } } impl Default for Storage { @@ -52,10 +63,18 @@ impl Storage { Some(Self::proj_dir()?.data_dir().join(SAVED_PLANS)) } + pub fn history_dir() -> Option { + Some(Self::proj_dir()?.data_dir().join(SAVED_HISTORIES)) + } + pub fn config_file() -> Option { Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE)) } + pub fn history_cache_file() -> Option { + Some(Self::proj_dir()?.data_dir().join(HISTORY_CACHE_FILE)) + } + pub fn save_config(&self) { let content = match serde_json::to_string_pretty(&self) { Ok(content) => content, @@ -145,7 +164,10 @@ impl Storage { }; let mut read_dir: Vec = match plan_dir.read_dir() { - Ok(read_dir) => read_dir.filter_map(|v| v.ok()).collect(), + Ok(read_dir) => read_dir.filter_map(|v| { + log::trace!("Read dir: {:?}", v); + v.ok() + }).collect(), Err(_) => return vec![], }; @@ -185,9 +207,80 @@ impl Storage { } fn load_metadata_at_path(path: PathBuf) -> Option { - let mut plan: PlanMetadata = - serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok()?; + + let mut plan: PlanMetadata = match serde_json::from_str(&std::fs::read_to_string(&path).ok()?) { + Ok(plan) => plan, + Err(e) => { + log::error!("Could not load metadata at path: {path:?} : {e}"); + return None; + } + }; plan.set_stored_path(Some(path)); Some(plan) } + + pub fn save_history(history: RunHistory) { + Self::store_history_at_path(&history); + Self::store_history_at_cache(history); + } + + pub fn load_history_at_uuid(uuid: String) -> Option { + serde_json::from_str(&std::fs::read_to_string(Self::history_uuid_to_path(uuid)?).ok()?).ok() + } + + pub fn load_cache() -> Option> { + let path = match Self::history_cache_file() { + Some(path) => path, + None => { log::error!("Could not get path for history cache"); return None; }, + }; + + serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok() + } + + fn history_uuid_to_path(uuid: String) -> Option { + let history_dir = match Self::history_dir() { + Some(dir) => dir, + None => return None, + }; + + Some(history_dir.join(uuid.to_string()).with_extension("json")) + } + + fn store_history_at_path(history: &RunHistory) { + let path = match Self::history_uuid_to_path(history.uuid()) { + Some(path) => path, + None => { log::error!("Could not get path for history"); return; }, + }; + + let serialization_result = match serde_json::to_string(&history) { + Ok(serialization_result) => serialization_result, + Err(e) => { log::error!("Could not serialize history: {}", e); return;}, + }; + + std::fs::write( + path, + serialization_result, + ).ok(); + } + + fn store_history_at_cache(history: RunHistory) { + let mut cache = match Self::load_cache() { + Some(cache) => cache, + None => HashMap::new(), + }; + + cache.insert(history.uuid(), history.into()); + + let path = match Self::history_cache_file() { + Some(path) => path, + None => { log::error!("Could not get path for history cache"); return; }, + }; + + let serialization = match serde_json::to_string(&cache) { + Ok(serialization) => serialization, + Err(e) => { log::error!("Could not serialize history cache: {}", e); return; }, + }; + + std::fs::write(path, serialization).ok(); + } } diff --git a/src-tauri/src/time.rs b/src-tauri/src/time.rs new file mode 100644 index 0000000..86176c5 --- /dev/null +++ b/src-tauri/src/time.rs @@ -0,0 +1,51 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RunHistory { + entries: Vec, + + #[serde(flatten)] + metadata: RunHistoryMetadata, +} + +impl RunHistory { + pub fn uuid(&self) -> String { + self.metadata.uuid.clone() + } +} + +impl From for RunHistoryMetadata { + fn from(history: RunHistory) -> Self { + history.metadata + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum EntryType { + PlanForceNext, + PlanForcePrev, + ZoneEnter, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TrackEntry { + #[serde(rename = "type")] + entry_type: EntryType, + zone: String, + current_elapsed_millis: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RunHistoryMetadata { + uuid: String, + current_elapsed_millis: u64, + associated_name: String, + #[serde(default = "default_last_updated_date_now")] + last_updated: u64, +} + +fn default_last_updated_date_now() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d18d07d..e4ec7c6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Nothing", - "version": "1.5.2" + "version": "1.6.0" }, "tauri": { "systemTray": { diff --git a/src/app/_models/plan.ts b/src/app/_models/plan.ts index b006065..3045073 100644 --- a/src/app/_models/plan.ts +++ b/src/app/_models/plan.ts @@ -9,7 +9,7 @@ export interface PlanInterface { name?: string; latest_server_etag?: string; identifier?: string, - last_stored_time?: number; + last_stored_time?: string; } export interface PlanMetadata { @@ -18,7 +18,7 @@ export interface PlanMetadata { name: string; latest_server_etag?: string; identifier?: string; - last_stored_time?: number; + last_stored_time?: string; } export class Plan { @@ -28,7 +28,7 @@ export class Plan { name?: string; latest_server_etag?: string; identifier?: string; - last_stored_time?: number + last_stored_time?: string; private path?: string; diff --git a/src/app/_services/time-tracker.service.ts b/src/app/_services/time-tracker.service.ts new file mode 100644 index 0000000..1bb0bda --- /dev/null +++ b/src/app/_services/time-tracker.service.ts @@ -0,0 +1,281 @@ +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> = new ReplaySubject>(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('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>('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: [], + }); + } +} \ No newline at end of file diff --git a/src/app/plan-display/plan-display.component.html b/src/app/plan-display/plan-display.component.html index 32ccfa5..bd543a8 100644 --- a/src/app/plan-display/plan-display.component.html +++ b/src/app/plan-display/plan-display.component.html @@ -1,5 +1,6 @@
+
- {{currentTime()}} + {{currentTime(timeTrackerService.elapsedTimeMillis)}} + + + +
\ No newline at end of file diff --git a/src/app/plan-display/plan-display.component.scss b/src/app/plan-display/plan-display.component.scss index 6a1bf97..e4d668b 100644 --- a/src/app/plan-display/plan-display.component.scss +++ b/src/app/plan-display/plan-display.component.scss @@ -198,4 +198,10 @@ notes { svg { fill: red; } +} + +.stop-stopwatch { + position: absolute; + bottom: 0; + right: 0; } \ No newline at end of file diff --git a/src/app/plan-display/plan-display.component.ts b/src/app/plan-display/plan-display.component.ts index d5eaeb9..8d4450d 100644 --- a/src/app/plan-display/plan-display.component.ts +++ b/src/app/plan-display/plan-display.component.ts @@ -17,6 +17,7 @@ import { EventsService } from '../_services/events.service'; import { Event } from '@tauri-apps/api/event'; import { MatDialog } from '@angular/material/dialog'; import { ResumeDialog } from './resume-dialog.component'; +import { TimeTrackerService } from '../_services/time-tracker.service'; enum Resume { Discard, @@ -53,15 +54,9 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { previousPlans: PlanMetadata[] = []; checkingPlanUpdate: boolean = false; - start?: Date; - latest?: Date; - timerSubscription?: Subscription; - debouncedSaveStopwatch?: Subscription; - recentUpdateAttempts: Map = 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, public dialog: MatDialog) { + 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, public timeTrackerService: TimeTrackerService) { window.addEventListener("resize", () => { this.zone.run(() => { this.windowInitHandler() @@ -74,26 +69,18 @@ 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; + this.timeTrackerService.onNewRun(plan); //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; } @@ -107,43 +94,11 @@ 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(); @@ -256,8 +211,21 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { setupBinds() { if (this.currentSlides && !this.bindsAreSetup) { - this.nextBind = this.shortcut.register(this.configService.config.prev).subscribe((_shortcut) => this.prev()); - this.prevBind = this.shortcut.register(this.configService.config.next).subscribe((_shortcut) => this.next()); + + this.nextBind = this.shortcut.register(this.configService.config.prev).subscribe((_shortcut) => { + this.prev(); + if (this.configService.config.enableStopwatch) { + this.timeTrackerService.onForcePrev(this.currentPlan!.plan[this.currentPlan!.current].area_key); + } + }); + + this.prevBind = this.shortcut.register(this.configService.config.next).subscribe((_shortcut) => { + this.next(); + if (this.configService.config.enableStopwatch) { + this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key); + } + }); + this.bindsAreSetup = true; } } @@ -282,6 +250,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { this.currentPlan!.prev(); this.currentSlides?.prev(); this.zoneSlides?.prev(); + } } @@ -341,8 +310,10 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { onScroll(event: WheelEvent) { if (event.deltaY < 0) { this.prev(); + this.timeTrackerService.onForcePrev(this.currentPlan!.plan[this.currentPlan!.current].area_key); } else { this.next(); + this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key); } } @@ -422,46 +393,28 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit { return ""; } - currentTime() { - if (!this.latest || !this.start) return ""; + currentTime(elapsed?: number) { + if (!elapsed) 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'); + 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}`; } - 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; - } - }) + + return this.timeTrackerService.isActive; + } + + stopStopwatch() { + this.timeTrackerService.stop(); + } + + startStopwatch() { + this.timeTrackerService.startLoaded(); } } diff --git a/src/app/plan-display/resume-dialog.component.html b/src/app/plan-display/resume-dialog.component.html index 0223990..78e2450 100644 --- a/src/app/plan-display/resume-dialog.component.html +++ b/src/app/plan-display/resume-dialog.component.html @@ -1,6 +1,6 @@ -Resume instantaneously, on next zone enter or not at all? +You have an ongoing run history going, would you like to resume it?
- - - + + +
\ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2c1d11e..3852a89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2629,6 +2629,11 @@ dependencies: "@types/node" "*" +"@types/uuid@^9.0.8": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/ws@^8.5.1": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -7008,6 +7013,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"