More refactoring, some more aggregate stats

main
isark 1 year ago
parent 5c2d03272f
commit 9cf16ab3df

@ -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::<Mutex<Storage>>()
.lock()
.is_ok_and(|s| s.config.hide_on_unfocus)
{
fsm.handle(&State::Hidden {}.into())
{
fsm.handle(&State::Hidden {}.into())
}
}
}
UnderlayEvent::X11FullscreenEvent { is_fullscreen: _ } => {}

@ -148,6 +148,8 @@ export class PlanService {
private loadBasePlan() {
from(invoke<PlanInterface>('base_plan')).subscribe(plan => {
plan.plan.forEach(elem => { elem.edited = false; });
plan.current = 0;
plan.name = "Base Plan";
this._basePlanSubject?.next(new Plan(plan));
});
}

@ -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<string, UnformattedAggregateRunStat>();
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 : "",
};
}

@ -99,7 +99,7 @@ export class TimeTrackerService {
this.currentRunHistory?.entries.push(entry);
});
}
public getCurrentRunHistory(): Observable<RunHistory> {
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}`;
}

@ -1,4 +1,51 @@
<div>
Spent: {{currentSpent()}}
Compare spent: {{compareSpent()}}
</div>
<div *ngIf="latestEntry && timeTrackerService.isActive" class="d-flex flex-column">
<div class="zone"><b>{{resolveZone(latestEntry.zone)}}</b></div>
<div class="d-flex flex-row">
Spent:
<app-wrap-value [value]="{current: currentSpent(), compare: compareSpent()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
First Entry:
<app-wrap-value [value]="{current: currentFirst(), compare: compareFirst()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
Last Entry:
<app-wrap-value [value]="{current: currentLast(), compare: compareLast()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current | hms}}</span>
<span>{{v.compare | hms}}</span>
</ng-template>
</app-wrap-value>
</div>
<div class="d-flex flex-row">
Number of Entries:
<app-wrap-value [value]="{current: currentEntries(), compare: compareEntries()}">
<ng-template let-v>
<span class="mx-2" [class]="classCurrentLowBetter(v.current, v.compare)">{{v.current}}</span>
<span>{{v.compare}}</span>
</ng-template>
</app-wrap-value>
</div>
</div>
<ng-container *ngIf="!latestEntry">
Awaiting zone entry to compare data
</ng-container>
<ng-container *ngIf="latestEntry && !timeTrackerService.isActive">
Awaiting stopwatch to start
</ng-container>

@ -0,0 +1,9 @@
.indeterminate {
color: orange;
}
.good-diff {
color: rgb(46, 179, 46);
}
.bad-diff {
color: rgb(199, 13, 13);
}

@ -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<string, WorldArea>;
private currentRunHistory?: RunHistory;
private compareAggregate?: Map<string, UnformattedAggregateRunStat>;
private currentAggregate?: Map<string, UnformattedAggregateRunStat>;
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<string, UnformattedAggregateRunStat>(this.aggregatedRunHistory.aggregation.map(agg => [agg.zoneId, agg]));
this.compareAggregate = new Map<string, UnformattedAggregateRunStat>(
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;
}
}

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

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

@ -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)'">
<app-aggregate-display></app-aggregate-display>
isark
</app-draggable-window>
<app-draggable-window [hidden]="!configService.config.detachNotes"
[initialRect]="configService.config.initialNotesWindowPosition" [configurable]="overlayService.interactable"
(savedRect)="configService.config.initialNotesWindowPosition = $event"
[backgroundColor]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'">
<div *ngIf="configService.config.detachNotes" class="standalone-notes">
<carousel [initIndex]="currentPlan.current" [slides]="currentPlan.plan"
(afterInitSelf)="registerCurrentSlides($event)">
@ -102,49 +100,7 @@
[cdkConnectedOverlayOrigin]="globalTopLeft" (detach)="settingsOpen = false">
<div class="overlay container-fluid vw-100">
<div class="row row-cols-2 h-100">
<div class="planChooser col-xs-6 col-sm-6 col-md-6 col-lg-4 col-xl-4 d-flex flex-column">
<div class="d-flex justify-content-evenly">
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="openDialog()">Browse
Plans
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadBasePlan()">
Load base plan
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadFromUrl()">
Import from url
</button>
</div>
</div>
<div class="enumerated d-flex flex-column">
<mat-list role="list" class="d-flex flex-column h-100">
<cdk-virtual-scroll-viewport itemSize="10" class="h-100">
<mat-list-item class="d-flex flex-column" role="listitem"
*cdkVirtualFor="let plan of previousPlans">
<span>
<button [disabled]="disablePlans" (click)="loadPrevious(plan.stored_path!)">
<img *ngIf="plan.update_url" src="assets/public.svg">{{plan.name}}
</button><button *ngIf="plan.update_url" class="planActionButton"
[disabled]="disablePlans" (click)="checkForPlanUpdate(plan)"
[title]="recentPlanTitle(plan)">
<svg-icon *ngIf="!hasUpdate(plan)" src="assets/material-sync.svg" />
<svg-icon *ngIf="hasUpdate(plan) && !isErr(plan)"
src="assets/material-check.svg" class="nice" />
<svg-icon *ngIf="hasUpdate(plan) && isErr(plan)"
src="assets/material-warning.svg" class="notnice" />
</button>
</span>
</mat-list-item>
</cdk-virtual-scroll-viewport>
</mat-list>
</div>
</div>
<app-plan-selection class="planChooser col-xs-6 col-sm-6 col-md-6 col-lg-4 col-xl-4 d-flex flex-column"></app-plan-selection>
<settings class="col-xs-6 col-sm-6 offset-md-1 col-md-5 offset-lg-4 col-lg-4 offset-xl-4 col-xl-4">
</settings>
</div>
@ -152,9 +108,5 @@
</div>
</ng-template>
<ng-container *ngIf="overlayService.interactable">
<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>
<app-stopwatch-controls class="stop-stopwatch"></app-stopwatch-controls>
</ng-container>

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

@ -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<string, Date | UrlError> = new Map<string, Date | UrlError>();
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<StateEvent>("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this));
this.events.listen<boolean>("overlay_or_target_focus").subscribe((event: Event<boolean>) => {
if (event.payload) {
this.destroyBinds();
} else {
this.setupBinds();
}
});
}
onOverlayStateChange(event: Event<StateEvent>) {
@ -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;
}

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

@ -0,0 +1,37 @@
<div class="d-flex justify-content-evenly align-items-center mb-2 mt-2">
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="openDialog()">Browse
Plans
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadBasePlan()">
Load base plan
</button>
</div>
<div class="col-xs-4">
<button class="" mat-raised-button color="accent" (click)="loadFromUrl()">
Import from url
</button>
</div>
</div>
<div class="enumerated d-flex flex-column">
<mat-list role="list" class="d-flex flex-column h-100">
<cdk-virtual-scroll-viewport itemSize="10" class="h-100">
<mat-list-item class="d-flex flex-column" role="listitem" *cdkVirtualFor="let plan of plans">
<span>
<button [disabled]="disablePlans" (click)="loadPrevious(plan.stored_path!)">
<img *ngIf="plan.update_url" src="assets/public.svg">{{plan.name}}
</button><button *ngIf="plan.update_url" class="planActionButton" [disabled]="disablePlans"
(click)="checkForPlanUpdate(plan)" [title]="recentPlanTitle(plan)">
<svg-icon *ngIf="!hasUpdate(plan)" src="assets/material-sync.svg" />
<svg-icon *ngIf="hasUpdate(plan) && !isErr(plan)" src="assets/material-check.svg"
class="nice" />
<svg-icon *ngIf="hasUpdate(plan) && isErr(plan)" src="assets/material-warning.svg"
class="notnice" />
</button>
</span>
</mat-list-item>
</cdk-virtual-scroll-viewport>
</mat-list>
</div>

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

@ -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<PlanSelectionComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PlanSelectionComponent]
});
fixture = TestBed.createComponent(PlanSelectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<string, Date | UrlError> = new Map<string, Date | UrlError>();
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;
}
}

@ -1,4 +1,4 @@
You have an ongoing run history going, would you like to resume it?
You have an ongoing run history <b>(time tracking)</b> going, would you like to resume it?
<div mat-dialog-actions>
<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>

@ -0,0 +1,5 @@
<button mat-raised-button color="warn" *ngIf="timeTrackerService.isActive"
(click)="timeTrackerService.stop()">Force stop stopwatch</button>
<button mat-raised-button color="warn"
*ngIf="!timeTrackerService.isActive && timeTrackerService.hasRunLoaded" (click)="timeTrackerService.startLoaded()">Start last
loaded</button>

@ -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<StopwatchControlsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [StopwatchControlsComponent]
});
fixture = TestBed.createComponent(StopwatchControlsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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

@ -0,0 +1,2 @@
<ng-container *ngTemplateOutlet="template!; context: { $implicit: value }">
</ng-container>

@ -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<WrapValueComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [WrapValueComponent]
});
fixture = TestBed.createComponent(WrapValueComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<T> {
@Input() value?: T;
@ContentChild(TemplateRef) template?: TemplateRef<any>;
}
Loading…
Cancel
Save