diff --git a/src/timer-service.ts b/src/timer-service.ts index 98f8da0..ffbe806 100644 --- a/src/timer-service.ts +++ b/src/timer-service.ts @@ -85,6 +85,33 @@ export class TimerService { return this.state; } + public updateActiveStart(startedAtEpochMs: number): TrackerState { + const active = this.state.activeTimer; + if (!active) { + return this.state; + } + + if (!Number.isFinite(startedAtEpochMs)) { + throw new Error("Invalid start timestamp."); + } + + const now = nowMs(); + if (startedAtEpochMs > now) { + throw new Error("Start must be less than or equal to now."); + } + + this.state = { + ...this.state, + activeTimer: { + ...active, + startedAtEpochMs + } + }; + + this.persist(); + return this.state; + } + public switchToPage(page: PageRef): TrackerState { if (this.state.activeTimer?.pageUuid === page.uuid) { return this.state; diff --git a/src/ui.ts b/src/ui.ts index 5a00080..d2114ca 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -54,6 +54,8 @@ export class TimetrackerUi { private readonly listPages: () => Promise; private intervalId: number | null = null; private editDraft: EditDraft | null = null; + private activeStartDraft: string | null = null; + private isEditingActiveStart = false; private pageOptions: PageRef[] = []; constructor( @@ -71,8 +73,8 @@ export class TimetrackerUi { this.bindEvents(); this.render(); this.intervalId = window.setInterval(() => { - // Keep focus stable while an entry is being edited. - if (this.editDraft) { + // Keep focus stable while any form field is being edited. + if (this.editDraft || this.isEditingActiveStart) { return; } this.render(); @@ -90,7 +92,13 @@ export class TimetrackerUi { const app = this.ensureContainer(); const state = this.service.getState(); const active = state.activeTimer; - const activeDuration = active ? Date.now() - active.startedAtEpochMs : 0; + const activeDuration = active ? Math.max(0, Date.now() - active.startedAtEpochMs) : 0; + + if (!active && this.activeStartDraft !== null) { + this.activeStartDraft = null; + } + + const activeStartValue = active ? this.activeStartDraft ?? toIsoLocalInput(active.startedAtEpochMs) : ""; const daySummaries = computeDaySummaries(state.entries); const weekSummaries = computeWeekSummaries(state.entries); @@ -139,6 +147,20 @@ export class TimetrackerUi { + ${active + ? `
+ Active timer +
+ +
+
+ +
+
` + : ""} ${this.editDraft ? this.renderEditPanel() : ""} @@ -250,6 +272,11 @@ export class TimetrackerUi { return; } + if (target.id === "tt-active-start-save") { + this.saveActiveStartDraft(); + return; + } + if (target.id === "tt-edit-save") { this.saveEditDraft(); return; @@ -274,7 +301,15 @@ export class TimetrackerUi { document.addEventListener("input", (event) => { const target = event.target as HTMLInputElement | HTMLTextAreaElement | null; - if (!target || !this.editDraft) { + if (!target) { + return; + } + + if (target.id === "tt-active-start") { + this.activeStartDraft = target.value; + } + + if (!this.editDraft) { return; } @@ -291,6 +326,20 @@ export class TimetrackerUi { } }); + document.addEventListener("focusin", (event) => { + const target = event.target as HTMLElement | null; + if (target?.id === "tt-active-start") { + this.isEditingActiveStart = true; + } + }); + + document.addEventListener("focusout", (event) => { + const target = event.target as HTMLElement | null; + if (target?.id === "tt-active-start") { + this.isEditingActiveStart = false; + } + }); + document.addEventListener("keydown", (event) => { if (event.key === "Escape") { if (this.editDraft) { @@ -377,6 +426,32 @@ export class TimetrackerUi { } } + private saveActiveStartDraft(): void { + const active = this.service.getState().activeTimer; + if (!active) { + this.activeStartDraft = null; + this.render(); + return; + } + + const rawInput = this.activeStartDraft ?? toIsoLocalInput(active.startedAtEpochMs); + const startedAtEpochMs = parseLocalInput(rawInput); + + if (!Number.isFinite(startedAtEpochMs)) { + logseq.App.showMsg("Invalid date format.", "error"); + return; + } + + try { + this.service.updateActiveStart(startedAtEpochMs); + this.activeStartDraft = null; + this.render(); + logseq.App.showMsg("Active start updated.", "success"); + } catch (error) { + logseq.App.showMsg((error as Error).message, "error"); + } + } + private escapeHtml(value: string): string { return value .replace(/&/g, "&")