From 5db8663819de625b3252e3a4fd98302d0083d212 Mon Sep 17 00:00:00 2001 From: isark Date: Sun, 17 Mar 2024 12:21:07 +0100 Subject: [PATCH] Improved time tracking support practically 'recording' your whole playthrough during a plan. In practice this means we're recording every zone entry, the elapsed time at that time and also recording any time you manually shift the plan as well. Later on I'll look into creating some views of this data e.g. an aggregate view e.g. time spent grouped by each zone and such --- package.json | 2 + src-tauri/Cargo.lock | 10 +- src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 29 +- src-tauri/src/plan.rs | 20 +- src-tauri/src/storage.rs | 99 +++++- src-tauri/src/time.rs | 51 ++++ src-tauri/tauri.conf.json | 2 +- src/app/_models/plan.ts | 6 +- src/app/_services/time-tracker.service.ts | 281 ++++++++++++++++++ .../plan-display/plan-display.component.html | 6 +- .../plan-display/plan-display.component.scss | 6 + .../plan-display/plan-display.component.ts | 119 +++----- .../plan-display/resume-dialog.component.html | 8 +- yarn.lock | 10 + 15 files changed, 545 insertions(+), 105 deletions(-) create mode 100644 src-tauri/src/time.rs create mode 100644 src/app/_services/time-tracker.service.ts 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"