You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
13 KiB
442 lines
13 KiB
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<PlanElement>;
|
|
currentSlides?: CarouselComponent<PlanElement>;
|
|
worldAreaMap?: Map<String, WorldArea>;
|
|
settingsOpen: boolean = false;
|
|
hasAttachedOnce: boolean = false;
|
|
|
|
overlayStateChangeHandle?: Subscription;
|
|
bindsAreSetup: boolean = false;
|
|
nextBind?: Subscription;
|
|
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,
|
|
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<StateEvent>("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this));
|
|
}
|
|
|
|
onOverlayStateChange(event: Event<StateEvent>) {
|
|
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<PlanElement>) {
|
|
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<PlanElement>) {
|
|
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;
|
|
}
|