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
main 1.6.0
isark 1 year ago
parent fda12b2f90
commit 5db8663819

@ -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"
},

10
src-tauri/Cargo.lock generated

@ -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",

@ -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

@ -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<PlanMetadata> {
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<RunHistory> {
Storage::load_history_at_uuid(uuid)
}
#[tauri::command]
fn load_cache() -> Option<HashMap<String, RunHistoryMetadata>> {
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<PathBuf>, 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();
// }
}
}
}

@ -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<String>,
latest_server_etag: Option<String>,
identifier: Option<String>,
last_stored_time: Option<u64>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_option_string")]
last_stored_time: Option<String>,
}
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<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let v: Option<Value> = Option::deserialize(deserializer)?;
Ok(match v {
Some(Value::String(s)) => Some(s),
Some(Value::Number(n)) => Some(n.to_string()),
_ => None,
})
}
impl From<PlanMetadata> for Option<Plan> {
fn from(metadata: PlanMetadata) -> Self {
Some(serde_json::from_slice(&std::fs::read(metadata.stored_path?).ok()?).ok()?)

@ -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<PathBuf> = 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<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(SAVED_HISTORIES))
}
pub fn config_file() -> Option<PathBuf> {
Some(Self::proj_dir()?.data_dir().join(CONFIG_FILE))
}
pub fn history_cache_file() -> Option<PathBuf> {
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<DirEntry> = 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<PlanMetadata> {
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<RunHistory> {
serde_json::from_str(&std::fs::read_to_string(Self::history_uuid_to_path(uuid)?).ok()?).ok()
}
pub fn load_cache() -> Option<HashMap<String, RunHistoryMetadata>> {
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<PathBuf> {
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();
}
}

@ -0,0 +1,51 @@
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RunHistory {
entries: Vec<TrackEntry>,
#[serde(flatten)]
metadata: RunHistoryMetadata,
}
impl RunHistory {
pub fn uuid(&self) -> String {
self.metadata.uuid.clone()
}
}
impl From<RunHistory> 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
}

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

@ -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;

@ -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<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: [],
});
}
}

@ -1,5 +1,6 @@
<ng-container *ngIf="init">
<div #globalTopLeft style="position: fixed; top: 0; left: 0; z-index: -1;"></div>
<div class=""></div>
<ng-container *ngIf="rect && currentPlan">
<div class="target waypoint trial"
[style.background-color]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'"
@ -46,7 +47,7 @@
zones</span>
</div>
</tooltip>
<span *ngIf="shouldDisplayTimer()" class="timer">{{currentTime()}}</span>
<span *ngIf="shouldDisplayTimer()" class="timer">{{currentTime(timeTrackerService.elapsedTimeMillis)}}</span>
</div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"
@ -111,4 +112,7 @@
(click)="settingsOpen = false"><span>+</span></button>
</div>
</ng-template>
<button class="stop-stopwatch" mat-raised-button color="warn" *ngIf="timeTrackerService.isActive" (click)="stopStopwatch()">Force stop stopwatch</button>
<button class="stop-stopwatch" mat-raised-button color="warn" *ngIf="!timeTrackerService.isActive && timeTrackerService.hasRunLoaded" (click)="startStopwatch()">Start last loaded</button>
</ng-container>

@ -198,4 +198,10 @@ notes {
svg {
fill: red;
}
}
.stop-stopwatch {
position: absolute;
bottom: 0;
right: 0;
}

@ -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<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, 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();
}
}

@ -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?
<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>
<button mat-raised-button color="color-resume-instant" (click)="instant()">Resume instantaneously</button>
<button mat-raised-button color="color-resume-next" (click)="next()" cdkFocusInitial>Resume on entering next zone</button>
<button mat-raised-button color="color-resume-no" (click)="discard()" cdkFocusInitial>Start new</button>
</div>

@ -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"

Loading…
Cancel
Save