parent
5c2d03272f
commit
9cf16ab3df
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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…
Reference in new issue