import { ChangeDetectorRef, 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 { 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'; @Component({ selector: 'plan-display', templateUrl: './plan-display.component.html', styleUrls: ['./plan-display.component.scss'] }) export class PlanDisplayComponent implements OnInit { @Input() backgroundColor?: string; slideIndex: number = 0; zoneSlides?: CarouselComponent; currentSlides?: CarouselComponent; worldAreaMap?: Map; settingsOpen: boolean = false; hasAttachedOnce: boolean = false; overlayStateChangeHandle?: Subscription; bindsAreSetup: boolean = false; nextBind?: Subscription; prevBind?: Subscription; currentPlan?: Plan; previousPlans: PlanMetadata[] = []; checkingPlanUpdate: boolean = false; recentUpdateAttempts: Map = new Map(); aggregatedRunHistory?: UnformattedAggregationData; latestZoneId?: string; constructor( public configService: ConfigService, public planService: PlanService, public worldAreaService: WorldAreaService, public overlayService: OverlayService, public dialog: MatDialog, 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; if (this.configService.config.enableStopwatch) { this.loadComparisonData(this.currentPlan); } this.timeTrackerService.onNewRun(plan); //Close settings anytime we get a new current plan. this.settingsOpen = false; setTimeout(() => this.setIndex(plan.current), 0); }) this.registerOnZoneEnter(); } loadComparisonData(plan: Plan) { if (!this.configService.config.runCompareHistory) { return; } 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()); } } }); } ngOnInit() { this.worldAreaService.getFullWorldAreas().subscribe(a => this.worldAreaMap = a); this.overlayStateChangeHandle = this.events.listen("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this)); } onOverlayStateChange(event: Event) { if (event.payload.Hidden) { this.destroyBinds(); } else { this.setupBinds(); } } destroyBinds() { if (this.bindsAreSetup) { this.nextBind?.unsubscribe(); this.prevBind?.unsubscribe(); this.bindsAreSetup = false; } } abs(v: number) { return Math.abs(v); } hasWaypoint(key?: string): boolean { if (!key) { key = this.currentPlan!.plan[this.currentPlan!.current].area_key; } const world_area = this.worldAreaMap?.get(key); return world_area!.has_waypoint; } hasTrial(key?: string): boolean { if (!key) { key = this.currentPlan!.plan[this.currentPlan!.current].area_key; } return this.worldAreaService.hasTrial(key); } registerZoneSlides(carousel: CarouselComponent) { this.zoneSlides = carousel; this.zoneSlides.setIndex(this.slideIndex); } setupBinds() { if (this.currentSlides && !this.bindsAreSetup) { 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.checkCheckpoint(); } }); 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.checkCheckpoint(); } }); this.bindsAreSetup = true; } } registerCurrentSlides(carousel: CarouselComponent) { this.currentSlides = carousel; this.currentSlides.setIndex(this.slideIndex); this.setupBinds(); } next() { if (this.overlayService.visible) { this.currentPlan!.next(); this.checkCheckpoint(); this.currentSlides?.next(); this.zoneSlides?.next(); } } checkCheckpoint() { if (!this.currentPlan || !this.timeTrackerService.isActive) return; const currentElem = this.currentPlan.plan[this.currentPlan.current]; if (currentElem.checkpoint && !currentElem.checkpoint_your_millis) { currentElem.checkpoint_your_millis = this.timeTrackerService.elapsedTimeMillis; this.timeTrackerService.reportCheckpoint(currentElem.uuid!); } } yourDiff(element: PlanElement) { if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return ""; const diff = element.checkpoint_your_millis - element.checkpoint_millis; const neg = diff <= 0; const abs = Math.abs(diff); if (diff == 0) { return `${neg ? "-" : "+"}00:00:00`; } else { return `${neg ? "-" : "+"}${this.timeTrackerService.hmsTimestamp(abs)}`; } } yourDiffClass(element: PlanElement): string { if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return ""; const diff = element.checkpoint_your_millis - element.checkpoint_millis; const neg = diff <= 0; return neg ? "negative-diff" : "positive-diff"; } showDiff(element: PlanElement) { return element.checkpoint && element.checkpoint_your_millis && element.checkpoint_millis; } cpMillis(element: PlanElement) { if (!element.checkpoint) return ""; if (!element.checkpoint_millis) return "N/A"; return this.timeTrackerService.hmsTimestamp(element.checkpoint_millis); } prev() { if (this.overlayService.visible) { this.currentPlan!.prev(); this.currentSlides?.prev(); this.zoneSlides?.prev(); } } setIndex(index: number) { this.slideIndex = index; if (this.currentSlides) { this.currentSlides.setIndex(index); } if (this.zoneSlides) { this.zoneSlides.setIndex(index); } } 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(); this.timeTrackerService.onForcePrev(this.currentPlan!.plan[this.currentPlan!.current].area_key); this.checkCheckpoint(); } else { this.next(); this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key); this.checkCheckpoint(); } } specialClasses() { const waypoint = this.hasWaypoint() ? 'active' : ''; const trial = this.hasTrial() ? 'trial-active' : ''; return `${waypoint} ${trial}`; } clampedOffset(): number { return Math.min(this.configService.config.numVisible - 1, this.configService.config.offset); } zonesStyle() { return { 'min-height': `${this.configService.config.numVisible * 18}px`, 'max-height': `${this.configService.config.numVisible * 40}px` } } 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) { return this.trim(this.trimUnneccesaryWords(zoneName)); } else { return zoneName; } } trimUnneccesaryWords(zoneName: string) { if (zoneName.toLowerCase().startsWith("the ")) { return zoneName.substring(4); } else { return zoneName; } } trim(zoneName: string, letters: number = 12) { if (zoneName.length > letters + 3) { return zoneName.substring(0, letters) + "..."; } 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; }