Startzeitpunkt manipulierbar

This commit is contained in:
Eric Neuber 2026-06-26 10:15:10 +02:00
parent 35659829c9
commit d63f8cb7b2
2 changed files with 106 additions and 4 deletions

View File

@ -85,6 +85,33 @@ export class TimerService {
return this.state; 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 { public switchToPage(page: PageRef): TrackerState {
if (this.state.activeTimer?.pageUuid === page.uuid) { if (this.state.activeTimer?.pageUuid === page.uuid) {
return this.state; return this.state;

View File

@ -54,6 +54,8 @@ export class TimetrackerUi {
private readonly listPages: () => Promise<PageRef[]>; private readonly listPages: () => Promise<PageRef[]>;
private intervalId: number | null = null; private intervalId: number | null = null;
private editDraft: EditDraft | null = null; private editDraft: EditDraft | null = null;
private activeStartDraft: string | null = null;
private isEditingActiveStart = false;
private pageOptions: PageRef[] = []; private pageOptions: PageRef[] = [];
constructor( constructor(
@ -71,8 +73,8 @@ export class TimetrackerUi {
this.bindEvents(); this.bindEvents();
this.render(); this.render();
this.intervalId = window.setInterval(() => { this.intervalId = window.setInterval(() => {
// Keep focus stable while an entry is being edited. // Keep focus stable while any form field is being edited.
if (this.editDraft) { if (this.editDraft || this.isEditingActiveStart) {
return; return;
} }
this.render(); this.render();
@ -90,7 +92,13 @@ export class TimetrackerUi {
const app = this.ensureContainer(); const app = this.ensureContainer();
const state = this.service.getState(); const state = this.service.getState();
const active = state.activeTimer; 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 daySummaries = computeDaySummaries(state.entries);
const weekSummaries = computeWeekSummaries(state.entries); const weekSummaries = computeWeekSummaries(state.entries);
@ -139,6 +147,20 @@ export class TimetrackerUi {
<button id="tt-stop">Stop active</button> <button id="tt-stop">Stop active</button>
<button id="tt-export">Export JSON</button> <button id="tt-export">Export JSON</button>
</div> </div>
${active
? `<div class="tt-edit-panel">
<strong>Active timer</strong>
<div class="tt-edit-grid">
<label>
<span>Start</span>
<input id="tt-active-start" type="datetime-local" value="${this.escapeAttribute(activeStartValue)}" />
</label>
</div>
<div class="tt-actions">
<button id="tt-active-start-save">Save active start</button>
</div>
</div>`
: ""}
${this.editDraft ? this.renderEditPanel() : ""} ${this.editDraft ? this.renderEditPanel() : ""}
<table> <table>
<thead> <thead>
@ -250,6 +272,11 @@ export class TimetrackerUi {
return; return;
} }
if (target.id === "tt-active-start-save") {
this.saveActiveStartDraft();
return;
}
if (target.id === "tt-edit-save") { if (target.id === "tt-edit-save") {
this.saveEditDraft(); this.saveEditDraft();
return; return;
@ -274,7 +301,15 @@ export class TimetrackerUi {
document.addEventListener("input", (event) => { document.addEventListener("input", (event) => {
const target = event.target as HTMLInputElement | HTMLTextAreaElement | null; 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; 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) => { document.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
if (this.editDraft) { 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 { private escapeHtml(value: string): string {
return value return value
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")