Mildly improved support for livesplit inspired functionality. Also initial beginnings of supporting an "aggregated" live stat for your ongoing run compared to another one.

main
isark 1 year ago
parent a8443964bc
commit 7c3e739f8d

@ -26,6 +26,8 @@ pub struct Rect {
pub struct Config {
#[serde(default = "Config::default_initial_plan_window_position")]
pub initial_plan_window_position: Rect,
#[serde(default = "Config::default_initial_agg_window_position")]
pub initial_agg_window_position: Rect,
#[serde(default = "Config::default_hide_on_unfocus")]
pub hide_on_unfocus: bool,
#[serde(default = "Config::default_toggle_overlay")]
@ -53,12 +55,20 @@ pub struct Config {
#[serde(default = "Config::default_run_compare_history")]
pub run_compare_history: Option<String>,
#[serde(default = "Config::default_show_livesplit")]
show_livesplit: bool,
#[serde(default = "Config::default_show_live_aggregate")]
show_live_aggregate: bool,
#[serde(default = "Config::default_shorten_zone_names")]
shorten_zone_names: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
initial_plan_window_position: Self::default_initial_plan_window_position(),
initial_agg_window_position: Self::default_initial_agg_window_position(),
hide_on_unfocus: Self::default_hide_on_unfocus(),
toggle_overlay: Self::default_toggle_overlay(),
prev: Self::default_prev(),
@ -72,8 +82,11 @@ impl Default for Config {
note_default_fg: Self::default_note_default_fg(),
num_visible: Self::default_plan_num_visible(),
offset: Self::default_plan_offset(),
enable_stopwatch: Self::default_enable_stopwatch(),
run_compare_history: Self::default_run_compare_history(),
enable_stopwatch: Self::default_enable_stopwatch(),
show_livesplit: Self::default_show_livesplit(),
show_live_aggregate: Self::default_show_live_aggregate(),
shorten_zone_names: Self::default_shorten_zone_names(),
}
}
}
@ -83,6 +96,10 @@ impl Config {
Default::default()
}
fn default_initial_agg_window_position() -> Rect {
Default::default()
}
fn default_hide_on_unfocus() -> bool {
true
}
@ -133,4 +150,13 @@ impl Config {
fn default_run_compare_history() -> Option<String> {
None
}
fn default_show_livesplit() -> bool {
false
}
fn default_show_live_aggregate() -> bool {
false
}
fn default_shorten_zone_names() -> bool {
false
}
}

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { EntryType, RunHistory } from './time-tracker.service';
import { EntryType, RunHistory, TrackEntry } from './time-tracker.service';
import { Plan } from '../_models/plan';
@ -51,6 +51,9 @@ 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;

@ -71,6 +71,7 @@ export class RunHistory {
providedIn: 'root'
})
export class TimeTrackerService {
private currentRunHistory?: RunHistory;
private timerSubscription?: Subscription;
@ -84,12 +85,32 @@ export class TimeTrackerService {
private storedHistoriesSubject: Subject<Map<string, RunHistoryMetadata>> = new ReplaySubject<Map<string, RunHistoryMetadata>>(1);
private pushSubject: Subject<TrackEntry> = new ReplaySubject<TrackEntry>(1);
private newCurrentRunHistorySubject: Subject<RunHistory> = new ReplaySubject<RunHistory>(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);
});
this.pushSubject.subscribe((entry) => {
this.currentRunHistory?.entries.push(entry);
});
}
public getCurrentRunHistory(): Observable<RunHistory> {
return this.newCurrentRunHistorySubject.asObservable();
}
getLatestEntry() {
return this.pushSubject.asObservable();
}
private setCurrentRunHistory(history: RunHistory) {
this.currentRunHistory = history;
this.newCurrentRunHistorySubject.next(history);
}
get elapsedTimeMillis() {
@ -120,24 +141,24 @@ export class TimeTrackerService {
this.loadHistory(plan.last_stored_time).subscribe(history => {
if (history) {
this.currentRunHistory = history;
this.setCurrentRunHistory(history);
} else {
//Legacy or missing history, attempt to preserve elapsed time
this.currentRunHistory = this.createNew(plan.name);
plan.last_stored_time = this.currentRunHistory.uuid;
this.setCurrentRunHistory(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;
this.currentRunHistory!.currentElapsedMillis = old_time;
}
plan.requestSelfSave();
}
this.currentRunHistory.plan = plan;
this.currentRunHistory!.plan = plan;
this.askResume(plan);
});
} else {
this.currentRunHistory = this.createNew(plan.name);
this.currentRunHistory.plan = plan;
this.setCurrentRunHistory(this.createNew(plan.name));
this.currentRunHistory!.plan = plan;
}
}
@ -162,7 +183,6 @@ export class TimeTrackerService {
this.timerSubscription = timer(0, 1000).subscribe(() => {
this.zone.run(() => {
this.latest = new Date();
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
});
});
@ -174,15 +194,14 @@ export class TimeTrackerService {
private underlyingSaveStopwatch() {
if (this.currentRunHistory && this.active && this.start && this.latest) {
console.log("Underlying save!");
this.currentRunHistory!.currentElapsedMillis = this.elapsedTimeMillis;
this.currentRunHistory!.last_updated = Date.now();
this.saveHistory(this.currentRunHistory!).subscribe(() => { });
this.loadCache();
if(this.currentRunHistory.plan) {
if (this.currentRunHistory.plan) {
this.currentRunHistory.plan.last_stored_time = this.currentRunHistory.uuid;
this.currentRunHistory.plan.requestSelfSave();
}
@ -205,7 +224,7 @@ export class TimeTrackerService {
public onForceNext(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.currentRunHistory?.entries.push({
this.pushSubject.next({
type: EntryType.PlanForceNext,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
@ -217,7 +236,7 @@ export class TimeTrackerService {
public onForcePrev(forced_area: string) {
if (this.configService.config.enableStopwatch) {
if (this.isActive) {
this.currentRunHistory?.entries.push({
this.pushSubject.next({
type: EntryType.PlanForcePrev,
zone: forced_area,
current_elapsed_millis: this.elapsedTimeMillis
@ -228,7 +247,7 @@ export class TimeTrackerService {
//Not perfect but good enough..
public reportCheckpoint(checkpoint: string) {
this.currentRunHistory?.entries.push({
this.pushSubject.next({
type: EntryType.CheckpointReached,
zone: checkpoint,
current_elapsed_millis: this.elapsedTimeMillis
@ -291,7 +310,7 @@ export class TimeTrackerService {
this.startStopwatch();
}
this.currentRunHistory?.entries.push({
this.pushSubject.next({
type: EntryType.ZoneEnter,
zone: zone,
current_elapsed_millis: this.elapsedTimeMillis
@ -307,19 +326,36 @@ export class TimeTrackerService {
switch (resume) {
case Resume.Instant:
this.startStopwatch();
this.loadReachedCheckpoints();
break;
case Resume.Next:
this.resumeOnNext = true;
this.loadReachedCheckpoints();
break;
case Resume.Discard:
this.currentRunHistory = this.createNew(plan.name);
plan.last_stored_time = this.currentRunHistory.uuid;
this.setCurrentRunHistory(this.createNew(plan.name));
plan.last_stored_time = this.currentRunHistory!.uuid;
plan.requestSelfSave();
break;
}
})
}
loadReachedCheckpoints() {
if (!this.currentRunHistory || !this.currentRunHistory.plan || !this.configService.config.runCompareHistory) return;
this.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
if (!history) return;
const checkpoints = new Map(history.entries.filter(entry => entry.type === EntryType.CheckpointReached).map(entry => [entry.zone, entry.current_elapsed_millis]));
const ourCheckpointValidZones = new Map(this.currentRunHistory?.plan?.plan.filter(entry => checkpoints.has(entry.uuid!)).map(entry => [entry.uuid!, entry]));
this.currentRunHistory?.entries.filter(entry => entry.type === EntryType.CheckpointReached).forEach(entry => {
if (ourCheckpointValidZones.has(entry.zone)) {
ourCheckpointValidZones.get(entry.zone)!.checkpoint_your_millis = entry.current_elapsed_millis;
}
})
});
}
private createNew(associatedName?: string) {
const uuid = uuidv4();

@ -0,0 +1,4 @@
<div>
Spent: {{currentSpent()}}
Compare spent: {{compareSpent()}}
</div>

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AggregateDisplayComponent } from './aggregate-display.component';
describe('AggregateDisplayComponent', () => {
let component: AggregateDisplayComponent;
let fixture: ComponentFixture<AggregateDisplayComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AggregateDisplayComponent]
});
fixture = TestBed.createComponent(AggregateDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,104 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlanService } from '../_services/plan.service';
import { EntryType, RunHistory, TimeTrackerService, TrackEntry } from '../_services/time-tracker.service';
import { Plan } from '../_models/plan';
import { ConfigService } from '../_services/config.service';
import { RunStatService, UnformattedAggregateRunStat, UnformattedAggregationData } from '../_services/run-stat.service';
@Component({
selector: 'app-aggregate-display',
standalone: true,
imports: [CommonModule],
templateUrl: './aggregate-display.component.html',
styleUrls: ['./aggregate-display.component.scss']
})
export class AggregateDisplayComponent {
private aggregatedRunHistory?: UnformattedAggregationData;
private currentHistory?: RunHistory;
private latestEntry?: TrackEntry;
private compareAggregate?: Map<string, UnformattedAggregateRunStat>;
private currentAggregate?: Map<string, UnformattedAggregateRunStat>;
constructor(
private planSerive: PlanService,
public timeTrackerService: TimeTrackerService,
private configService: ConfigService,
private runStatService: RunStatService
) {
this.timeTrackerService.getCurrentRunHistory().subscribe(history => {
this.currentHistory = history;
this.aggregatedRunHistory = this.runStatService.calcAggregated(history);
this.loadComparisonData();
});
this.timeTrackerService.getLatestEntry().subscribe(entry => {
if (entry.type != EntryType.ZoneEnter) return;
if (this.latestEntry) {
this.expandAggregated(this.latestEntry, entry);
}
this.latestEntry = entry;
});
}
private loadComparisonData() {
if (!this.configService.config.runCompareHistory) {
return;
}
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]));
}
});
}
expandAggregated(oldEntry: TrackEntry, newEntry: TrackEntry) {
if (!this.currentAggregate) return;
let aggregate: UnformattedAggregateRunStat = {
zoneId: oldEntry.zone,
aggregateFirstEntry: oldEntry.current_elapsed_millis,
aggregateLastExit: newEntry.current_elapsed_millis,
aggregateTimeSpent: (newEntry.current_elapsed_millis - oldEntry.current_elapsed_millis),
aggregateNumEntries: 1,
}
const existing = this.currentAggregate.get(oldEntry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
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) {
return this.timeTrackerService.hmsTimestamp(value);
} else {
return "N/A";
}
}
compareSpent() {
if (!this.latestEntry) return "N/A";
const value = this.compareAggregate?.get(this.latestEntry?.zone)?.aggregateTimeSpent;
if(value) {
return this.timeTrackerService.hmsTimestamp(value);
} else {
return "N/A";
}
}
}

@ -56,7 +56,7 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
@Input() offset: number = 0;
containerDirectionLength: number = 0;
private debouncedOnchange: Subject<void> = new Subject<void>();
constructor(private cdr: ChangeDetectorRef) {
this.visibleSlides = [];
this.debouncedOnchange.pipe(debounceTime(500)).subscribe(() => this.realOnChange());
@ -65,27 +65,26 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
}
}
ngOnInit(): void {
this.afterInitSelf.next(this);
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
let changed = false;
entries.forEach(entry => {
const runIntersectionHandling = () => {
const entryIndex = parseInt(entry.target.getAttribute('data-slideIndex')!);
if (!entryIndex && entryIndex != 0) {
return;
}
const entryIntersectingSlide = this.visibleSlides?.find(s => s.index == entryIndex);
if (!entryIntersectingSlide) {
return;
}
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
};
runIntersectionHandling();
const entryIndex = parseInt(entry.target.getAttribute('data-slideIndex')!);
if (!entryIndex && entryIndex != 0) {
return;
}
const entryIntersectingSlide = this.visibleSlides?.find(s => s.index == entryIndex);
if (!entryIntersectingSlide) {
return;
}
entryIntersectingSlide.currentlyIntersecting = entry.isIntersecting;
});
if (changed) {
this.onChange();
})
}
})
}
@ -98,7 +97,7 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
}
ngOnChanges(changes: SimpleChanges): void {
if(changes['numVisible'] || changes['offset']) {
if (changes['numVisible'] || changes['offset']) {
this.reinitializeVisibleSlides();
}
}
@ -138,10 +137,10 @@ export class CarouselComponent<T> implements OnInit, AfterViewInit, OnChanges {
const start = Math.max(0, this.current - this.numExtraPrev());
const end = Math.min(this.current + this.numExtraNext(), this.slides!.length - 1);
for (let i = start; i <= end; i++) {
this.visibleSlides?.push({
index: i,
currentlyIntersecting: false,
});
this.visibleSlides?.push({
index: i,
currentlyIntersecting: false,
});
}
this.onChange();

@ -2,62 +2,94 @@
<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)'"
[style.transform]="transform()" [style.width]="rect.width + 'px'" [style.height]="rect.height + 'px'"
[class]="specialClasses()" (wheel)="onScroll($event)" #targetRef>
<ng-container *ngIf="currentPlan">
<carousel class="zones" [initIndex]="currentPlan.current" [numVisible]="configService.config.numVisible"
[offset]="clampedOffset()" [slides]="currentPlan.plan" (afterInitSelf)="registerZoneSlides($event)"
[ngStyle]="zonesStyle()">
<ng-template let-slide let-index="index">
<div class="zone-slide" [style.color]="configService.config.noteDefaultFg"
[style.border]="index == currentPlan.current ? '1px white solid' : 'none'">
<div *ngIf="showDiff(slide)">
<div style="margin: 0 3px;" [class]="yourDiffClass(slide)">{{yourDiff(slide)}}</div>
</div>
<div style="margin: 0 5px">{{worldAreaMap!.get(slide.area_key)!.name}}</div>
<div class="text-marker d-flex flex-row">
<div style="margin: 0 3px;" >{{cpMillis(slide)}}</div>
<div class="waypoint-text" *ngIf="hasWaypoint(slide.area_key)">(W)</div>
<div class="trial-text" *ngIf="hasTrial(slide.area_key)">(T)</div>
<div style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none;">
<div class="target waypoint trial"
[style.background-color]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'"
[style.transform]="transform()" [style.width]="rect.width + 'px'" [style.height]="rect.height + 'px'"
[class]="specialClasses()" (wheel)="onScroll($event)" #targetRef>
<ng-container *ngIf="currentPlan">
<carousel class="zones" [initIndex]="currentPlan.current"
[numVisible]="configService.config.numVisible" [offset]="clampedOffset()"
[slides]="currentPlan.plan" (afterInitSelf)="registerZoneSlides($event)"
[ngStyle]="zonesStyle()">
<ng-template let-slide let-index="index">
<div class="zone-slide" [style.color]="configService.config.noteDefaultFg"
[style.border]="index == currentPlan.current ? '1px white solid' : 'none'">
<div class="text-marker-left d-flex flex-row"
*ngIf="configService.config.showLivesplit && showDiff(slide)">
<div style="margin: 0 3px;" [class]="yourDiffClass(slide)">{{yourDiff(slide)}}</div>
</div>
<div style="margin: 0 5px">{{displayZoneName(worldAreaMap!.get(slide.area_key)!.name)}}
</div>
<div class="text-marker d-flex flex-row">
<div *ngIf="configService.config.showLivesplit" style="margin: 0 3px;">
{{cpMillis(slide)}}</div>
<div class="waypoint-text" *ngIf="hasWaypoint(slide.area_key)">(W)</div>
<div class="trial-text" *ngIf="hasTrial(slide.area_key)">(T)</div>
</div>
</div>
</div>
</ng-template>
</carousel>
<carousel [initIndex]="currentPlan.current" [slides]="currentPlan.plan"
(afterInitSelf)="registerCurrentSlides($event)">
<ng-template let-slide>
<scalable [clamp]="2">
<notes class="p-1" [note]="slide.notes" [style.color]="configService.config.noteDefaultFg"
#noteSlide></notes>
</scalable>
</ng-template>
</carousel>
</ng-template>
</carousel>
<carousel [initIndex]="currentPlan.current" [slides]="currentPlan.plan"
(afterInitSelf)="registerCurrentSlides($event)">
<ng-template let-slide>
<scalable [clamp]="2">
<notes class="p-1" [note]="slide.notes"
[style.color]="configService.config.noteDefaultFg" #noteSlide></notes>
</scalable>
</ng-template>
</carousel>
</ng-container>
<button mat-icon-button *ngIf="overlayService.interactable" class="settings-button"
(click)="settingsClick($event)" (mousedown)="$event.stopPropagation()"><img
src="assets/material-settings.svg"></button>
<tooltip *ngIf="overlayService.interactable" class="help">
<div class="d-flex flex-column help-area">
<span><span class="waypoint-text">(W)</span> = Waypoint</span>
<span><span class="trial-text">(T)</span> = Trial</span>
<span>The plan window's will have a glow in the corresponding color(s) above to help indicate if the
current zone has any of those.</span>
<span>You can scroll in the plan window (while it is in 'interactable' mode) to quickly switch many
zones</span>
</div>
</tooltip>
<span *ngIf="shouldDisplayTimer()" class="timer">{{timeTrackerService.hmsTimestamp(timeTrackerService.elapsedTimeMillis)}}</span>
</ng-container>
<button mat-icon-button *ngIf="overlayService.interactable" class="settings-button"
(click)="settingsClick($event)" (mousedown)="$event.stopPropagation()"><img
src="assets/material-settings.svg"></button>
<tooltip *ngIf="overlayService.interactable" class="help">
<div class="d-flex flex-column help-area">
<span><span class="waypoint-text">(W)</span> = Waypoint</span>
<span><span class="trial-text">(T)</span> = Trial</span>
<span>The plan window's will have a glow in the corresponding color(s) above to help indicate if
the
current zone has any of those.</span>
<span>You can scroll in the plan window (while it is in 'interactable' mode) to quickly switch
many
zones</span>
</div>
</tooltip>
<span *ngIf="shouldDisplayTimer()"
class="timer">{{timeTrackerService.hmsTimestamp(timeTrackerService.elapsedTimeMillis)}}</span>
</div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"
[resizable]="true && overlayService.interactable" (drag)="onDrag($event)" (resize)="onResize($event)"
(dragEnd)="onDragEnd($event)" (resizeEnd)="onResizeEnd($event)" [bounds]="bounds" [snappable]="true"
[style.visibility]="overlayService.interactable ? 'visible' : 'hidden'"></ngx-moveable>
</div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"
[resizable]="true && overlayService.interactable" (drag)="onDrag($event)" (resize)="onResize($event)"
(dragEnd)="onDragEnd($event)" (resizeEnd)="onResizeEnd($event)" [bounds]="bounds" [snappable]="true"
[style.visibility]="overlayService.interactable ? 'visible' : 'hidden'"></ngx-moveable>
<ng-container *ngIf="configService.config.showLiveAggregate && rectAgg">
<div style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none;">
<div class="target-aggregate" [style.transform]="transformAgg()" [style.width]="rectAgg.width + 'px'"
[style.height]="rectAgg.height + 'px'"
[style.background-color]="backgroundColor ? backgroundColor : 'rgba(0, 0, 0, 0.1)'"
#aggregateTargetRef>
<app-aggregate-display></app-aggregate-display>
</div>
<ngx-moveable #moveable2 [target]="aggregateTargetRef"
[draggable]="draggable && overlayService.interactable"
[resizable]="true && overlayService.interactable" (drag)="onDragAgg($event)"
(resize)="onResizeAgg($event)" (dragEnd)="onDragEndAgg($event)" (resizeEnd)="onResizeEndAgg($event)"
[bounds]="boundsAgg" [snappable]="true"
[style.visibility]="overlayService.interactable && configService.config.showLiveAggregate ? 'visible' : 'hidden'"></ngx-moveable>
</div>
</ng-container>
</ng-container>
@ -117,6 +149,9 @@
</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>
<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>

@ -25,12 +25,38 @@
}
}
.target-aggregate {
min-width: 50px;
min-height: 50px;
display: flex;
flex-direction: column;
user-select: none;
z-index: -1;
&>* {
flex: 1 1 200px;
&:first-child {
flex: 1 1 15px;
min-height: 50px;
max-height: 120px;
}
}
}
.text-marker {
position: absolute;
right: 10px;
gap: 2px;
}
.text-marker-left {
position: absolute;
left: 10px;
bottom: 0px;
gap: 2px;
}
.waypoint-text {
color: rgba(25, 255, 255, 0.5);
}
@ -86,7 +112,7 @@
}
.zone-slide {
display: relative;
position: relative;
height: 100%;
display: flex;
align-items: center;

@ -17,7 +17,7 @@ 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 } from '../_services/run-stat.service';
import { AggregateRunStat, RunStatService, UnformattedAggregateRunStat, UnformattedAggregationData } from '../_services/run-stat.service';
@Component({
selector: 'plan-display',
@ -28,8 +28,11 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
@Input() backgroundColor?: String;
draggable: boolean = true;
rect?: Rect;
rectAgg?: Rect;
bounds: any = { "left": 0, "top": 0, "right": 0, "bottom": 0, "position": "css" };
boundsAgg: any = { "left": 0, "top": 0, "right": 0, "bottom": 0, "position": "css" };
@ViewChild("moveable") moveable?: NgxMoveableComponent;
@ViewChild("moveable2") moveable2?: NgxMoveableComponent;
slideIndex: number = 0;
zoneSlides?: CarouselComponent<PlanElement>;
@ -50,6 +53,9 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>();
aggregatedRunHistory?: UnformattedAggregationData;
latestZoneId?: string;
constructor(
public configService: ConfigService,
public planService: PlanService,
@ -78,7 +84,6 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
this.currentPlan = plan;
if (this.configService.config.enableStopwatch) {
console.log(configService.config.runCompareHistory);
this.loadComparisonData(this.currentPlan);
}
this.timeTrackerService.onNewRun(plan);
@ -110,8 +115,11 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
registerOnZoneEnter() {
appWindow.listen("entered", (entered) => {
if (this.currentPlan && typeof entered.payload == "string" && this.currentPlan.isNext(entered.payload)) {
this.zone.run(() => this.next());
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());
}
}
});
}
@ -123,7 +131,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
}
ngOnInit() {
this.worldAreaService.getWorldAreas().subscribe(a => this.worldAreaMap = a);
this.worldAreaService.getFullWorldAreas().subscribe(a => this.worldAreaMap = a);
this.overlayStateChangeHandle = this.events.listen<StateEvent>("OverlayStateChange").subscribe(this.onOverlayStateChange.bind(this));
}
@ -159,6 +167,18 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
return `${this.rect!.height}px`;
}
transformAgg() {
return `translate(${this.rectAgg!.x}px, ${this.rectAgg!.y}px)`;
}
widthAgg() {
return `${this.rectAgg!.width}px`;
}
heightAgg() {
return `${this.rectAgg!.height}px`;
}
hasWaypoint(key?: string): boolean {
if (!key) {
key = this.currentPlan!.plan[this.currentPlan!.current].area_key;
@ -178,6 +198,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
ngAfterViewInit(): void {
if (window.innerWidth > 0) {
const cfgRect = this.configService.config.initialPlanWindowPosition;
const cfgRectAgg = this.configService.config.initialAggWindowPosition;
this.rect = {
x: cfgRect.x * window.innerWidth,
@ -185,7 +206,16 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
width: cfgRect.width * window.innerWidth,
height: cfgRect.height * window.innerHeight,
}
this.rectAgg = {
x: cfgRectAgg.x * window.innerWidth,
y: cfgRectAgg.y * window.innerHeight,
width: cfgRectAgg.width * window.innerWidth,
height: cfgRectAgg.height * window.innerHeight,
}
this.moveable?.updateRect();
this.moveable2?.updateRect();
setTimeout(() => this.cdr.detectChanges(), 0);
this.init = true;
@ -211,6 +241,25 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
this.saveRect();
}
onDragAgg(e: OnDrag) {
this.rectAgg!.x = e.translate[0];
this.rectAgg!.y = e.translate[1];
}
onDragEndAgg(e: OnDragEnd) {
this.saveRectAgg();
}
onResizeAgg(e: OnResize) {
this.rectAgg!.width = e.width;
this.rectAgg!.height = e.height;
this.onDragAgg(e.drag);
}
onResizeEndAgg(e: OnResizeEnd) {
this.saveRectAgg();
}
saveRect() {
const toCfgRect = this.rect!;
this.configService.config.initialPlanWindowPosition = {
@ -221,6 +270,16 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
}
}
saveRectAgg() {
const toCfgRect = this.rectAgg!;
this.configService.config.initialAggWindowPosition = {
x: toCfgRect.x / window.innerWidth,
y: toCfgRect.y / window.innerHeight,
width: toCfgRect.width / window.innerWidth,
height: toCfgRect.height / window.innerHeight,
}
}
registerZoneSlides(carousel: CarouselComponent<PlanElement>) {
this.zoneSlides = carousel;
this.zoneSlides.setIndex(this.slideIndex);
@ -266,13 +325,13 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
}
checkCheckpoint() {
if(!this.currentPlan || !this.timeTrackerService.isActive) return;
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!);
}
if (currentElem.checkpoint && !currentElem.checkpoint_your_millis) {
currentElem.checkpoint_your_millis = this.timeTrackerService.elapsedTimeMillis;
this.timeTrackerService.reportCheckpoint(currentElem.uuid!);
}
}
yourDiff(element: PlanElement) {
@ -281,7 +340,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
const diff = element.checkpoint_your_millis - element.checkpoint_millis;
const neg = diff <= 0;
const abs = Math.abs(diff);
if(diff == 0) {
if (diff == 0) {
return `${neg ? "-" : "+"}00:00:00`;
} else {
return `${neg ? "-" : "+"}${this.timeTrackerService.hmsTimestamp(abs)}`;
@ -299,11 +358,11 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
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";
if (!element.checkpoint) return "";
if (!element.checkpoint_millis) return "N/A";
return this.timeTrackerService.hmsTimestamp(element.checkpoint_millis);
}
@ -469,6 +528,29 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
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 {

@ -18,6 +18,7 @@ import { AngularSvgIconModule } from 'angular-svg-icon';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatDialogModule } from '@angular/material/dialog';
import { ResumeDialog } from './resume-dialog.component';
import { AggregateDisplayComponent } from '../aggregate-display/aggregate-display.component';
@NgModule({
declarations: [
PlanDisplayComponent
@ -41,7 +42,8 @@ import { ResumeDialog } from './resume-dialog.component';
AngularSvgIconModule,
ScrollingModule,
MatDialogModule,
ResumeDialog
ResumeDialog,
AggregateDisplayComponent
],
exports: [

@ -60,9 +60,30 @@
</mat-form-field>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Shorten zone names</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.shortenZoneNames"></mat-slide-toggle>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable stopwatch</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.enableStopwatch"></mat-slide-toggle>
</div>
<ng-container *ngIf="configService.config.enableStopwatch">
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable displaying scuffed livesplit</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.showLivesplit"></mat-slide-toggle>
</div>
<div class="d-flex flex-row justify-content-between">
<span class="d-block">Enable aggregate-stats live compare</span>
<mat-slide-toggle [color]="overlayService.isOverlay ? 'primary-on-dark' : 'primary'"
[(ngModel)]="configService.config.showLiveAggregate"></mat-slide-toggle>
</div>
</ng-container>
</div>
Loading…
Cancel
Save