diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index c875585..8852986 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -58,7 +58,7 @@ impl Overlay { previous: State::hidden(), }; - window.manage(Mutex::new(OverlayData { })); + window.manage(Mutex::new(OverlayData {})); let mut fsm = Overlay::uninitialized_state_machine(overlay).init(); @@ -126,19 +126,22 @@ impl Overlay { UnderlayEvent::MoveResize(bounds) => fsm.handle(&Event::Bounds(bounds)), UnderlayEvent::Detach => fsm.handle(&State::hidden().into()), UnderlayEvent::Focus => { + fsm.window.emit("overlay_or_target_focus", true).ok(); if let State::Hidden {} = fsm.state() { fsm.handle(&State::Visible {}.into()); } } UnderlayEvent::Blur => { - if !fsm.window.is_focused().unwrap() - && fsm + if !fsm.window.is_focused().unwrap() { + fsm.window.emit("overlay_or_target_focus", false).ok(); + if fsm .window .state::>() .lock() .is_ok_and(|s| s.config.hide_on_unfocus) - { - fsm.handle(&State::Hidden {}.into()) + { + fsm.handle(&State::Hidden {}.into()) + } } } UnderlayEvent::X11FullscreenEvent { is_fullscreen: _ } => {} diff --git a/src/app/_services/plan.service.ts b/src/app/_services/plan.service.ts index 7b9fe0e..f384ce3 100644 --- a/src/app/_services/plan.service.ts +++ b/src/app/_services/plan.service.ts @@ -148,6 +148,8 @@ export class PlanService { private loadBasePlan() { from(invoke('base_plan')).subscribe(plan => { plan.plan.forEach(elem => { elem.edited = false; }); + plan.current = 0; + plan.name = "Base Plan"; this._basePlanSubject?.next(new Plan(plan)); }); } diff --git a/src/app/_services/run-stat.service.ts b/src/app/_services/run-stat.service.ts index 0d1e391..c2ea33b 100644 --- a/src/app/_services/run-stat.service.ts +++ b/src/app/_services/run-stat.service.ts @@ -51,12 +51,6 @@ export type RunStatType = RunStat | AggregateRunStat; providedIn: 'root' }) export class RunStatService { - reachedCheckpointTimes() { - - } - /// practically which zone can't have a last exit time as last exit is not determinable for the last entry - aggregateNAId?: string; - constructor() { } @@ -64,8 +58,6 @@ export class RunStatService { calcAggregated(data: RunHistory): UnformattedAggregationData { const aggregation = new Map(); - this.aggregateNAId = data.entries[data.entries.length - 1].zone; - data.entries.forEach((entry, index) => { const hasExit = !(data.entries.length - 1 === index); @@ -90,7 +82,7 @@ export class RunStatService { return { aggregation: Array.from(aggregation.values()), - aggregateNAId: this.aggregateNAId + aggregateNAId: data.entries.length > 0 ? data.entries[data.entries.length - 1].zone : "", }; } diff --git a/src/app/_services/time-tracker.service.ts b/src/app/_services/time-tracker.service.ts index f50d618..bbfc50e 100644 --- a/src/app/_services/time-tracker.service.ts +++ b/src/app/_services/time-tracker.service.ts @@ -99,7 +99,7 @@ export class TimeTrackerService { this.currentRunHistory?.entries.push(entry); }); } - + public getCurrentRunHistory(): Observable { return this.newCurrentRunHistorySubject.asObservable(); } @@ -112,7 +112,7 @@ export class TimeTrackerService { this.currentRunHistory = history; this.newCurrentRunHistorySubject.next(history); } - + get elapsedTimeMillis() { this.latest = new Date(); return this.latest!.valueOf() - this.start!.valueOf(); @@ -275,13 +275,7 @@ export class TimeTrackerService { } public hmsTimestamp(elapsed?: number): string { - if (!elapsed) return ""; - - 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}`; + return hmsTimestamp(elapsed); } public loadCache() { @@ -368,4 +362,14 @@ export class TimeTrackerService { entries: [], }); } +} + +export function hmsTimestamp(elapsed?: number): string { + if (elapsed == null || elapsed == undefined) return "N/A"; + + 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}`; } \ No newline at end of file diff --git a/src/app/aggregate-display/aggregate-display.component.html b/src/app/aggregate-display/aggregate-display.component.html index 7fc266d..a661008 100644 --- a/src/app/aggregate-display/aggregate-display.component.html +++ b/src/app/aggregate-display/aggregate-display.component.html @@ -1,4 +1,51 @@ -
- Spent: {{currentSpent()}} - Compare spent: {{compareSpent()}} -
\ No newline at end of file +
+
{{resolveZone(latestEntry.zone)}}
+
+ Spent: + + + {{v.current | hms}} + {{v.compare | hms}} + + +
+
+ First Entry: + + + {{v.current | hms}} + {{v.compare | hms}} + + +
+
+ Last Entry: + + + {{v.current | hms}} + {{v.compare | hms}} + + + + +
+
+ Number of Entries: + + + {{v.current}} + {{v.compare}} + + +
+
+ + + + + Awaiting zone entry to compare data + + + + Awaiting stopwatch to start + \ No newline at end of file diff --git a/src/app/aggregate-display/aggregate-display.component.scss b/src/app/aggregate-display/aggregate-display.component.scss index e69de29..ebd1778 100644 --- a/src/app/aggregate-display/aggregate-display.component.scss +++ b/src/app/aggregate-display/aggregate-display.component.scss @@ -0,0 +1,9 @@ +.indeterminate { + color: orange; +} +.good-diff { + color: rgb(46, 179, 46); +} +.bad-diff { + color: rgb(199, 13, 13); +} \ No newline at end of file diff --git a/src/app/aggregate-display/aggregate-display.component.ts b/src/app/aggregate-display/aggregate-display.component.ts index 266ff4d..eb90412 100644 --- a/src/app/aggregate-display/aggregate-display.component.ts +++ b/src/app/aggregate-display/aggregate-display.component.ts @@ -5,44 +5,63 @@ import { EntryType, RunHistory, TimeTrackerService, TrackEntry } from '../_servi import { Plan } from '../_models/plan'; import { ConfigService } from '../_services/config.service'; import { RunStatService, UnformattedAggregateRunStat, UnformattedAggregationData } from '../_services/run-stat.service'; +import { WorldArea } from '../_models/world-area'; +import { WorldAreaService } from '../_services/world-area.service'; +import { HmsPipe } from "../hms/hms.pipe"; +import { WrapValueComponent } from '../wrap-value/wrap-value.component'; +import { timer } from 'rxjs'; @Component({ - selector: 'app-aggregate-display', - standalone: true, - imports: [CommonModule], - templateUrl: './aggregate-display.component.html', - styleUrls: ['./aggregate-display.component.scss'] + selector: 'app-aggregate-display', + standalone: true, + templateUrl: './aggregate-display.component.html', + styleUrls: ['./aggregate-display.component.scss'], + imports: [CommonModule, HmsPipe, WrapValueComponent] }) export class AggregateDisplayComponent { - private aggregatedRunHistory?: UnformattedAggregationData; - private currentHistory?: RunHistory; - private latestEntry?: TrackEntry; + latestEntry?: TrackEntry; + private worldAreaMap?: Map; + private currentRunHistory?: RunHistory; private compareAggregate?: Map; private currentAggregate?: Map; constructor( - private planSerive: PlanService, public timeTrackerService: TimeTrackerService, private configService: ConfigService, - private runStatService: RunStatService + private runStatService: RunStatService, + private worldAreaService: WorldAreaService, ) { this.timeTrackerService.getCurrentRunHistory().subscribe(history => { - this.currentHistory = history; - this.aggregatedRunHistory = this.runStatService.calcAggregated(history); - this.loadComparisonData(); + this.onNewCurrent(history); }); + this.worldAreaService.getFullWorldAreas().subscribe((data) => { + this.worldAreaMap = data; + }) + this.timeTrackerService.getLatestEntry().subscribe(entry => { if (entry.type != EntryType.ZoneEnter) return; if (this.latestEntry) { - this.expandAggregated(this.latestEntry, entry); + this.expandCurrentAggregated(this.latestEntry, entry); } this.latestEntry = entry; }); } + private onNewCurrent(history: RunHistory) { + this.currentRunHistory = history; + this.loadComparisonData(); + const aggregatedRunHistory = this.runStatService.calcAggregated(history); + this.currentAggregate = new Map(aggregatedRunHistory.aggregation.map(agg => [agg.zoneId, agg])); + if (history.entries.length > 0) { + this.latestEntry = history.entries[history.entries.length - 1]; + } else { + this.latestEntry = undefined; + } + } + private loadComparisonData() { if (!this.configService.config.runCompareHistory) { return; @@ -50,15 +69,17 @@ export class AggregateDisplayComponent { this.timeTrackerService.loadHistory(this.configService.config.runCompareHistory).subscribe(history => { if (history) { - this.aggregatedRunHistory = this.runStatService.calcAggregated(history); - this.compareAggregate = new Map(this.aggregatedRunHistory.aggregation.map(agg => [agg.zoneId, agg])); + this.compareAggregate = new Map( + this.runStatService.calcAggregated(history).aggregation.map(agg => [agg.zoneId, agg]) + ); } }); } - expandAggregated(oldEntry: TrackEntry, newEntry: TrackEntry) { + private expandCurrentAggregated(oldEntry: TrackEntry, newEntry: TrackEntry) { if (!this.currentAggregate) return; + //Expand with old entry let aggregate: UnformattedAggregateRunStat = { zoneId: oldEntry.zone, aggregateFirstEntry: oldEntry.current_elapsed_millis, @@ -76,29 +97,59 @@ export class AggregateDisplayComponent { this.currentAggregate.set(aggregate.zoneId, existing ?? aggregate); } - - currentSpent() { - if (!this.latestEntry) return "N/A"; - - const value = this.currentAggregate?.get(this.latestEntry?.zone)?.aggregateTimeSpent ?? 0 + (this.timeTrackerService.elapsedTimeMillis - this.latestEntry?.current_elapsed_millis); - - if(value) { + + hms(value?: number) { + if (value) { return this.timeTrackerService.hmsTimestamp(value); } else { return "N/A"; } } + + resolveZone(zoneId: string) { + const area = this.worldAreaMap?.get(zoneId); + if (!area) { + return "Unknown zone: " + zoneId; + } - compareSpent() { - if (!this.latestEntry) return "N/A"; - const value = this.compareAggregate?.get(this.latestEntry?.zone)?.aggregateTimeSpent; + // Act might not be very reasonable but it's the best we have it.. + return area.name + " (A" + area.act + ")" + } + + classCurrentLowBetter(current?: number, compare?: number) { + if (compare == undefined || compare == null || current == undefined || current == null) return "indeterminate"; - if(value) { - return this.timeTrackerService.hmsTimestamp(value); - } else { - return "N/A"; - } + return current <= compare ? "good-diff" : "bad-diff"; + } + + currentSpent() { + return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateTimeSpent ?? 0) + (this.currentRunHistory!.currentElapsedMillis - this.latestEntry!.current_elapsed_millis); + } + + compareSpent() { + return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateTimeSpent; + } + + compareEntries() { + return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateNumEntries ?? 0; + } + currentEntries() { + return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateNumEntries ?? 0) + 1; + } + + compareLast() { + return this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateLastExit; + } + currentLast() { + return this.latestEntry?.current_elapsed_millis ?? 0; } + compareFirst() { + return (this.compareAggregate?.get(this.latestEntry!.zone)?.aggregateFirstEntry) ?? 0; + } + + currentFirst() { + return (this.currentAggregate?.get(this.latestEntry!.zone)?.aggregateFirstEntry) ?? 0; + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 54c4064..1467b31 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -18,6 +18,7 @@ import { TooltipComponent } from "./tooltip/tooltip.component"; import { HttpClientModule } from "@angular/common/http"; import { AngularSvgIconModule } from "angular-svg-icon"; import { RunStatsComponent } from "./run-stats/run-stats.component"; +import { WrapValueComponent } from './wrap-value/wrap-value.component'; // import { GemFinderComponent } from "./gem-finder/gem-finder.component"; export function initializeApp(configService: ConfigService) { @@ -28,7 +29,7 @@ export function initializeApp(configService: ConfigService) { @NgModule({ declarations: [ - AppComponent, + AppComponent ], imports: [ BrowserModule, diff --git a/src/app/hms/hms.pipe.ts b/src/app/hms/hms.pipe.ts new file mode 100644 index 0000000..36d8ba0 --- /dev/null +++ b/src/app/hms/hms.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { hmsTimestamp } from '../_services/time-tracker.service'; + +@Pipe({ + name: 'hms', + standalone: true +}) +export class HmsPipe implements PipeTransform { + transform(value?: number): string { + return hmsTimestamp(value); + } +} \ 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 71a2c10..b767d1c 100644 --- a/src/app/plan-display/plan-display.component.html +++ b/src/app/plan-display/plan-display.component.html @@ -70,16 +70,14 @@ [initialRect]="configService.config.initialAggWindowPosition" [configurable]="overlayService.interactable" (savedRect)="configService.config.initialAggWindowPosition = $event" [backgroundColor]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'"> - - isark - +
@@ -102,49 +100,7 @@ [cdkConnectedOverlayOrigin]="globalTopLeft" (detach)="settingsOpen = false">
-
-
-
- -
-
- -
-
- -
-
-
- - - - - - - - - - - -
-
+
@@ -152,9 +108,5 @@
- - + \ 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 8a7ba11..7c645b5 100644 --- a/src/app/plan-display/plan-display.component.scss +++ b/src/app/plan-display/plan-display.component.scss @@ -199,40 +199,6 @@ notes { 'opsz' 48 } -.planChooser { - overflow: hidden; -} - -.enumerated { - display: block; - overflow: hidden; - flex-grow: 1; - background-color: rgba(200, 200, 220, 0.15); - border: 1px solid rgba(255, 255, 255, 0.3); - - mat-list { - max-height: 100%; - overflow-y: scroll; - } -} - -.planActionButton { - align-items: center; - justify-content: center; - padding-bottom: 1px; -} - -.nice { - svg { - fill: green; - } -} - -.notnice { - svg { - fill: red; - } -} .stop-stopwatch { position: absolute; diff --git a/src/app/plan-display/plan-display.component.ts b/src/app/plan-display/plan-display.component.ts index 742697f..d57b001 100644 --- a/src/app/plan-display/plan-display.component.ts +++ b/src/app/plan-display/plan-display.component.ts @@ -1,20 +1,19 @@ -import { ChangeDetectorRef, Component, Input, NgZone, OnInit } from '@angular/core'; +import { Component, Input, NgZone, OnInit } from '@angular/core'; import { ConfigService } from '../_services/config.service'; import { ShortcutService } from '../_services/shortcut.service'; import { CarouselComponent } from '../carousel/carousel.component'; -import { PlanService, UrlError } from '../_services/plan.service'; -import { Plan, PlanElement, PlanMetadata } from '../_models/plan'; +import { PlanService } from '../_services/plan.service'; +import { Plan, PlanElement } from '../_models/plan'; import { WorldAreaService } from '../_services/world-area.service'; import { WorldArea } from '../_models/world-area'; import { Subscription, from } from 'rxjs'; -import { open } from '@tauri-apps/api/dialog'; import { OverlayService, StateEvent } from '../_services/overlay.service'; import { appWindow } from '@tauri-apps/api/window'; import { EventsService } from '../_services/events.service'; import { Event } from '@tauri-apps/api/event'; import { MatDialog } from '@angular/material/dialog'; import { TimeTrackerService } from '../_services/time-tracker.service'; -import { RunStatService, UnformattedAggregationData } from '../_services/run-stat.service'; +import { RunStatService } from '../_services/run-stat.service'; @Component({ selector: 'plan-display', @@ -37,13 +36,6 @@ export class PlanDisplayComponent implements OnInit { prevBind?: Subscription; currentPlan?: Plan; - previousPlans: PlanMetadata[] = []; - checkingPlanUpdate: boolean = false; - - recentUpdateAttempts: Map = new Map(); - - aggregatedRunHistory?: UnformattedAggregationData; - latestZoneId?: string; constructor( public configService: ConfigService, @@ -54,15 +46,10 @@ export class PlanDisplayComponent implements OnInit { public timeTrackerService: TimeTrackerService, private events: EventsService, - private cdr: ChangeDetectorRef, private shortcut: ShortcutService, private zone: NgZone, private runStatService: RunStatService, ) { - - this.planService.getStoredPlans().subscribe(plans => { - this.previousPlans = plans; - }) this.planService.getCurrentPlan().subscribe(plan => { this.currentPlan = plan; @@ -89,19 +76,15 @@ export class PlanDisplayComponent implements OnInit { this.timeTrackerService.loadHistory(this.configService.config.runCompareHistory).subscribe(history => { if (history) { this.runStatService.insertTimesAtCheckpoints(history, plan); - this.aggregatedRunHistory = this.runStatService.calcAggregated(history) } }); } - get disablePlans(): boolean { - return this.checkingPlanUpdate; - } + registerOnZoneEnter() { appWindow.listen("entered", (entered) => { if (this.currentPlan && typeof entered.payload == "string") { - this.zone.run(() => this.latestZoneId = (entered.payload as string)); if (this.currentPlan.isNext(entered.payload)) { this.zone.run(() => this.next()); } @@ -109,10 +92,17 @@ export class PlanDisplayComponent implements OnInit { }); } - ngOnInit() { this.worldAreaService.getFullWorldAreas().subscribe(a => this.worldAreaMap = a); this.overlayStateChangeHandle = this.events.listen("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this)); + this.events.listen("overlay_or_target_focus").subscribe((event: Event) => { + if (event.payload) { + this.destroyBinds(); + } else { + this.setupBinds(); + } + + }); } onOverlayStateChange(event: Event) { @@ -255,49 +245,13 @@ export class PlanDisplayComponent implements OnInit { } } - loadPrevious(path: string) { - this.planService.loadPlanFromPath(path, false).subscribe(plan => { - this.planService.setCurrentPlan(plan); - }); - } + settingsClick(event: any) { this.settingsOpen = !this.settingsOpen; event.stopPropagation(); } - openDialog() { - from(open({ - multiple: false, - filters: [ - { - name: "JSON (.json)", - extensions: ['json'] - } - ] - })).subscribe(file => { - if (file) { - this.planService.loadPlanFromPath(file as string).subscribe(plan => { - if (plan) { - this.planService.setCurrentPlan(plan); - } - }); - } - }); - } - - loadBasePlan() { - this.planService.getBasePlan().subscribe(plan => { - this.currentPlan = new Plan(plan); - if (this.zoneSlides) { - this.zoneSlides.setIndex(0); - } - if (this.currentSlides) { - this.currentSlides.setIndex(0); - } - }) - } - onScroll(event: WheelEvent) { if (event.deltaY < 0) { this.prev(); @@ -327,78 +281,12 @@ export class PlanDisplayComponent implements OnInit { } } - loadFromUrl() { - this.planService.loadFromUrl().subscribe(plan => { - if (plan) { - this.planService.savePlanAtStore(plan.name!, plan).subscribe((path) => { - if (path) { - plan.setPath(path); - } - }); - this.planService.setCurrentPlan(plan); - } - }); - } - - checkForPlanUpdate(plan: PlanMetadata) { - this.checkingPlanUpdate = true; - - this.planService.checkForPlanUpdate(plan).subscribe({ - next: updatedPlan => { - console.log("check update: ", updatedPlan); - this.planService.loadPlanFromPath(plan.stored_path!, false).subscribe(oldPlan => { - console.log("oldPlan", oldPlan); - const newCurrent = commonUUIDToNewCurrent(oldPlan, updatedPlan); - updatedPlan.current = newCurrent; - //TODO: Interface to determine if we want to save the plan or not (allow not doing it in case user regrets... Also needs position error fix). - this.planService.savePlanAtStore(oldPlan.name!, updatedPlan, true).subscribe(); - }) - }, - complete: () => { - this.checkingPlanUpdate = false - this.recentUpdateAttempts.set(plan.update_url!, new Date()); - }, - error: (err) => { - this.recentUpdateAttempts.set(plan.update_url!, err); - if (err instanceof UrlError) { - alert("Error retrieving plan update (" + err.status + "): " + err.message); - } else if (err instanceof Error) { - alert("Unexpected error: " + err.message); - } - - this.checkingPlanUpdate = false; - } - }); - } - - isErr(plan: PlanMetadata) { - return this.recentUpdateAttempts.get(plan.update_url!) instanceof UrlError; - } - - hasUpdate(plan: PlanMetadata): any { - return this.recentUpdateAttempts.has(plan.update_url!); - } - - recentPlanTitle(plan: PlanMetadata) { - if (!this.hasUpdate(plan)) return "Check for update"; - if (this.hasUpdate(plan) && !this.isErr(plan)) return "Up to date"; - if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating"; - return ""; - } - shouldDisplayTimer(): boolean { if (!this.configService.config.enableStopwatch) return false; return this.timeTrackerService.isActive; } - stopStopwatch() { - this.timeTrackerService.stop(); - } - - startStopwatch() { - this.timeTrackerService.startLoaded(); - } displayZoneName(zoneName: string) { if (this.configService.config.shortenZoneNames) { @@ -423,19 +311,3 @@ export class PlanDisplayComponent implements OnInit { return zoneName; } } - -function commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number { - let bestOldCurrent = oldPlan.current; - let newIndex = 0; - //Attempt current plan element's UUID first, otherwise step backwards until we find a match - while (bestOldCurrent >= 0) { - const tempNewIndex = updatedPlan.plan.findIndex(e => e.uuid === oldPlan.plan[bestOldCurrent].uuid); - if (tempNewIndex !== -1) { - newIndex = tempNewIndex; - break; - } - bestOldCurrent--; - } - - return newIndex; -} diff --git a/src/app/plan-display/plan-display.module.ts b/src/app/plan-display/plan-display.module.ts index e54758e..220101a 100644 --- a/src/app/plan-display/plan-display.module.ts +++ b/src/app/plan-display/plan-display.module.ts @@ -20,9 +20,11 @@ import { MatDialogModule } from '@angular/material/dialog'; import { ResumeDialog } from './resume-dialog.component'; import { AggregateDisplayComponent } from '../aggregate-display/aggregate-display.component'; import { DraggableWindowComponent } from '../draggable-window/draggable-window.component'; +import { StopwatchControlsComponent } from './stopwatch-controls/stopwatch-controls.component'; +import { PlanSelectionComponent } from './plan-selection/plan-selection.component'; @NgModule({ declarations: [ - PlanDisplayComponent + PlanDisplayComponent, ], imports: [ CommonModule, @@ -45,8 +47,9 @@ import { DraggableWindowComponent } from '../draggable-window/draggable-window.c MatDialogModule, ResumeDialog, AggregateDisplayComponent, - DraggableWindowComponent - + DraggableWindowComponent, + StopwatchControlsComponent, + PlanSelectionComponent, ], exports: [ PlanDisplayComponent diff --git a/src/app/plan-display/plan-selection/plan-selection.component.html b/src/app/plan-display/plan-selection/plan-selection.component.html new file mode 100644 index 0000000..a3f96de --- /dev/null +++ b/src/app/plan-display/plan-selection/plan-selection.component.html @@ -0,0 +1,37 @@ +
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + + + + +
\ No newline at end of file diff --git a/src/app/plan-display/plan-selection/plan-selection.component.scss b/src/app/plan-display/plan-selection/plan-selection.component.scss new file mode 100644 index 0000000..2f7895c --- /dev/null +++ b/src/app/plan-display/plan-selection/plan-selection.component.scss @@ -0,0 +1,34 @@ +.planChooser { + overflow: hidden; +} + +.enumerated { + display: block; + overflow: hidden; + flex-grow: 1; + background-color: rgba(200, 200, 220, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); + + mat-list { + max-height: 100%; + overflow-y: scroll; + } +} + +.nice { + svg { + fill: green; + } +} + +.notnice { + svg { + fill: red; + } +} + +.planActionButton { + align-items: center; + justify-content: center; + padding-bottom: 1px; +} \ No newline at end of file diff --git a/src/app/plan-display/plan-selection/plan-selection.component.spec.ts b/src/app/plan-display/plan-selection/plan-selection.component.spec.ts new file mode 100644 index 0000000..7249182 --- /dev/null +++ b/src/app/plan-display/plan-selection/plan-selection.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PlanSelectionComponent } from './plan-selection.component'; + +describe('PlanSelectionComponent', () => { + let component: PlanSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PlanSelectionComponent] + }); + fixture = TestBed.createComponent(PlanSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/plan-display/plan-selection/plan-selection.component.ts b/src/app/plan-display/plan-selection/plan-selection.component.ts new file mode 100644 index 0000000..ac65205 --- /dev/null +++ b/src/app/plan-display/plan-selection/plan-selection.component.ts @@ -0,0 +1,143 @@ +import { Component } from '@angular/core'; +import { Plan, PlanMetadata } from 'src/app/_models/plan'; +import { PlanService, UrlError } from 'src/app/_services/plan.service'; +import { open } from '@tauri-apps/api/dialog'; +import { from } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { MatListModule } from '@angular/material/list'; +import { AngularSvgIconModule } from 'angular-svg-icon'; + +@Component({ + selector: 'app-plan-selection', + templateUrl: './plan-selection.component.html', + styleUrls: ['./plan-selection.component.scss'], + standalone: true, + imports: [CommonModule, MatButtonModule, ScrollingModule, MatListModule, AngularSvgIconModule] +}) +export class PlanSelectionComponent { + plans: PlanMetadata[] = []; + checkingPlanUpdate: boolean = false; + + recentUpdateAttempts: Map = new Map(); + constructor( + private planService: PlanService, + ) { + this.planService.getStoredPlans().subscribe(plans => { + this.plans = plans; + }) + } + + get disablePlans(): boolean { + return this.checkingPlanUpdate; + } + + openDialog() { + from(open({ + multiple: false, + filters: [ + { + name: "JSON (.json)", + extensions: ['json'] + } + ] + })).subscribe(file => { + if (file) { + this.planService.loadPlanFromPath(file as string).subscribe(plan => { + if (plan) { + this.planService.setCurrentPlan(plan); + } + }); + } + }); + } + + loadBasePlan() { + this.planService.getBasePlan().subscribe(plan => { + plan.current = 0; + this.planService.setCurrentPlan(plan); + }); + } + + loadFromUrl() { + this.planService.loadFromUrl().subscribe(plan => { + if (plan) { + this.planService.savePlanAtStore(plan.name!, plan).subscribe((path) => { + if (path) { + plan.setPath(path); + } + }); + this.planService.setCurrentPlan(plan); + } + }); + } + + loadPrevious(path: string) { + this.planService.loadPlanFromPath(path, false).subscribe(plan => { + this.planService.setCurrentPlan(plan); + }); + } + + checkForPlanUpdate(plan: PlanMetadata) { + this.checkingPlanUpdate = true; + + this.planService.checkForPlanUpdate(plan).subscribe({ + next: updatedPlan => { + console.log("check update: ", updatedPlan); + this.planService.loadPlanFromPath(plan.stored_path!, false).subscribe(oldPlan => { + console.log("oldPlan", oldPlan); + const newCurrent = this.commonUUIDToNewCurrent(oldPlan, updatedPlan); + updatedPlan.current = newCurrent; + //TODO: Interface to determine if we want to save the plan or not (allow not doing it in case user regrets... Also needs position error fix). + this.planService.savePlanAtStore(oldPlan.name!, updatedPlan, true).subscribe(); + }) + }, + complete: () => { + this.checkingPlanUpdate = false + this.recentUpdateAttempts.set(plan.update_url!, new Date()); + }, + error: (err) => { + this.recentUpdateAttempts.set(plan.update_url!, err); + if (err instanceof UrlError) { + alert("Error retrieving plan update (" + err.status + "): " + err.message); + } else if (err instanceof Error) { + alert("Unexpected error: " + err.message); + } + + this.checkingPlanUpdate = false; + } + }); + } + + recentPlanTitle(plan: PlanMetadata) { + if (!this.hasUpdate(plan)) return "Check for update"; + if (this.hasUpdate(plan) && !this.isErr(plan)) return "Up to date"; + if (this.hasUpdate(plan) && this.isErr(plan)) return "Error updating"; + return ""; + } + + isErr(plan: PlanMetadata) { + return this.recentUpdateAttempts.get(plan.update_url!) instanceof UrlError; + } + + hasUpdate(plan: PlanMetadata): any { + return this.recentUpdateAttempts.has(plan.update_url!); + } + + private commonUUIDToNewCurrent(oldPlan: Plan, updatedPlan: Plan): number { + let bestOldCurrent = oldPlan.current; + let newIndex = 0; + //Attempt current plan element's UUID first, otherwise step backwards until we find a match + while (bestOldCurrent >= 0) { + const tempNewIndex = updatedPlan.plan.findIndex(e => e.uuid === oldPlan.plan[bestOldCurrent].uuid); + if (tempNewIndex !== -1) { + newIndex = tempNewIndex; + break; + } + bestOldCurrent--; + } + + return newIndex; + } +} diff --git a/src/app/plan-display/resume-dialog.component.html b/src/app/plan-display/resume-dialog.component.html index 78e2450..c953726 100644 --- a/src/app/plan-display/resume-dialog.component.html +++ b/src/app/plan-display/resume-dialog.component.html @@ -1,4 +1,4 @@ -You have an ongoing run history going, would you like to resume it? +You have an ongoing run history (time tracking) going, would you like to resume it?
diff --git a/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.html b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.html new file mode 100644 index 0000000..ad39680 --- /dev/null +++ b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.html @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.scss b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.spec.ts b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.spec.ts new file mode 100644 index 0000000..347e5e6 --- /dev/null +++ b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StopwatchControlsComponent } from './stopwatch-controls.component'; + +describe('StopwatchControlsComponent', () => { + let component: StopwatchControlsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StopwatchControlsComponent] + }); + fixture = TestBed.createComponent(StopwatchControlsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.ts b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.ts new file mode 100644 index 0000000..be7c189 --- /dev/null +++ b/src/app/plan-display/stopwatch-controls/stopwatch-controls.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { TimeTrackerService } from 'src/app/_services/time-tracker.service'; + +@Component({ + selector: 'app-stopwatch-controls', + templateUrl: './stopwatch-controls.component.html', + styleUrls: ['./stopwatch-controls.component.scss'], + imports: [CommonModule, MatButtonModule], + standalone: true +}) +export class StopwatchControlsComponent { + constructor(public timeTrackerService: TimeTrackerService) { } +} diff --git a/src/app/wrap-value/wrap-value.component.html b/src/app/wrap-value/wrap-value.component.html new file mode 100644 index 0000000..8ca8573 --- /dev/null +++ b/src/app/wrap-value/wrap-value.component.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/app/wrap-value/wrap-value.component.scss b/src/app/wrap-value/wrap-value.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/wrap-value/wrap-value.component.spec.ts b/src/app/wrap-value/wrap-value.component.spec.ts new file mode 100644 index 0000000..cdc7607 --- /dev/null +++ b/src/app/wrap-value/wrap-value.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WrapValueComponent } from './wrap-value.component'; + +describe('WrapValueComponent', () => { + let component: WrapValueComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [WrapValueComponent] + }); + fixture = TestBed.createComponent(WrapValueComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/wrap-value/wrap-value.component.ts b/src/app/wrap-value/wrap-value.component.ts new file mode 100644 index 0000000..6983265 --- /dev/null +++ b/src/app/wrap-value/wrap-value.component.ts @@ -0,0 +1,14 @@ +import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-wrap-value', + standalone: true, + imports: [CommonModule], + templateUrl: './wrap-value.component.html', + styleUrls: ['./wrap-value.component.scss'] +}) +export class WrapValueComponent { + @Input() value?: T; + @ContentChild(TemplateRef) template?: TemplateRef; +}