not change format semicolons

This commit is contained in:
hen 2024-11-14 19:44:04 -03:00
parent d4188127b3
commit 8aa3143793

View file

@ -1,150 +1,150 @@
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian" import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian";
import { SimpleTimeTrackerSettings } from "./settings" import { SimpleTimeTrackerSettings } from "./settings";
import { ConfirmModal } from "./confirm-modal" import { ConfirmModal } from "./confirm-modal";
export interface Tracker { export interface Tracker {
entries: Entry[] entries: Entry[];
} }
export interface Entry { export interface Entry {
name: string name: string;
startTime: string startTime: string;
endTime: string endTime: string;
subEntries?: Entry[] subEntries?: Entry[];
collapsed?: boolean collapsed?: boolean;
} }
export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> { export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> {
let file = app.vault.getAbstractFileByPath(fileName) as TFile let file = app.vault.getAbstractFileByPath(fileName) as TFile;
if (!file) if (!file)
return return;
let content = await app.vault.read(file) let content = await app.vault.read(file);
// figure out what part of the content we have to edit // figure out what part of the content we have to edit
let lines = content.split("\n") let lines = content.split("\n");
let prev = lines.filter((_, i) => i <= section.lineStart).join("\n") let prev = lines.filter((_, i) => i <= section.lineStart).join("\n");
let next = lines.filter((_, i) => i >= section.lineEnd).join("\n") let next = lines.filter((_, i) => i >= section.lineEnd).join("\n");
// edit only the code block content, leave the rest untouched // edit only the code block content, leave the rest untouched
content = `${prev}\n${JSON.stringify(tracker)}\n${next}` content = `${prev}\n${JSON.stringify(tracker)}\n${next}`;
await app.vault.modify(file, content) await app.vault.modify(file, content);
} }
export function loadTracker(json: string): Tracker { export function loadTracker(json: string): Tracker {
if (json) { if (json) {
try { try {
let ret = JSON.parse(json) let ret = JSON.parse(json);
updateLegacyInfo(ret.entries) updateLegacyInfo(ret.entries);
return ret return ret;
} catch (e) { } catch (e) {
console.log(`Failed to parse Tracker from ${json}`) console.log(`Failed to parse Tracker from ${json}`);
} }
} }
return { entries: [] } return { entries: [] };
} }
export async function loadAllTrackers(fileName: string): Promise<{ section: MarkdownSectionInformation, tracker: Tracker }[]> { export async function loadAllTrackers(fileName: string): Promise<{ section: MarkdownSectionInformation, tracker: Tracker }[]> {
let file = app.vault.getAbstractFileByPath(fileName) let file = app.vault.getAbstractFileByPath(fileName);
let content = (await app.vault.cachedRead(file as TFile)).split("\n") let content = (await app.vault.cachedRead(file as TFile)).split("\n");
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = [] let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = [];
let curr: Partial<MarkdownSectionInformation> | undefined let curr: Partial<MarkdownSectionInformation> | undefined;
for (let i = 0; i < content.length; i++) { for (let i = 0; i < content.length; i++) {
let line = content[i] let line = content[i];
if (line.trimEnd() == "```simple-time-tracker") { if (line.trimEnd() == "```simple-time-tracker") {
curr = { lineStart: i + 1, text: "" } curr = { lineStart: i + 1, text: "" };
} else if (curr) { } else if (curr) {
if (line.trimEnd() == "```") { if (line.trimEnd() == "```") {
curr.lineEnd = i - 1 curr.lineEnd = i - 1;
let tracker = loadTracker(curr.text) let tracker = loadTracker(curr.text);
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker }) trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker });
curr = undefined curr = undefined;
} else { } else {
curr.text += `${line}\n` curr.text += `${line}\n`;
} }
} }
} }
return trackers return trackers;
} }
type GetFile = () => string type GetFile = () => string;
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void { export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
element.addClass("simple-time-tracker-container") element.addClass("simple-time-tracker-container");
// add start/stop controls // add start/stop controls
let running = isRunning(tracker) let running = isRunning(tracker);
let btn = new ButtonComponent(element) let btn = new ButtonComponent(element)
.setClass("clickable-icon") .setClass("clickable-icon")
.setIcon(`lucide-${running ? "stop" : "play"}-circle`) .setIcon(`lucide-${running ? "stop" : "play"}-circle`)
.setTooltip(running ? "End" : "Start") .setTooltip(running ? "End" : "Start")
.onClick(async () => { .onClick(async () => {
if (running) { if (running) {
endRunningEntry(tracker) endRunningEntry(tracker);
} else { } else {
startNewEntry(tracker, newSegmentNameBox.getValue()) startNewEntry(tracker, newSegmentNameBox.getValue());
} }
await saveTracker(tracker, getFile(), getSectionInfo()) await saveTracker(tracker, getFile(), getSectionInfo());
}) });
btn.buttonEl.addClass("simple-time-tracker-btn") btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new TextComponent(element) let newSegmentNameBox = new TextComponent(element)
.setPlaceholder("Segment name") .setPlaceholder("Segment name")
.setDisabled(running) .setDisabled(running);
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt") newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
// add timers // add timers
let timer = element.createDiv({ cls: "simple-time-tracker-timers" }) let timer = element.createDiv({ cls: "simple-time-tracker-timers" });
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" }) let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" }) let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" });
currentDiv.createEl("span", { text: "Current" }) currentDiv.createEl("span", { text: "Current" });
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" }) let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" }) let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" });
totalDiv.createEl("span", { text: "Total" }) totalDiv.createEl("span", { text: "Total" });
let totalTodayDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" }) let totalTodayDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
let totalToday = totalTodayDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" }) let totalToday = totalTodayDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" })
totalTodayDiv.createEl("span", { text: "Total Today" }) totalTodayDiv.createEl("span", { text: "Total Today" })
if (tracker.entries.length > 0) { if (tracker.entries.length > 0) {
// add table // add table
let table = element.createEl("table", { cls: "simple-time-tracker-table" }) let table = element.createEl("table", { cls: "simple-time-tracker-table" });
table.createEl("tr").append( table.createEl("tr").append(
createEl("th", { text: "Segment" }), createEl("th", { text: "Segment" }),
createEl("th", { text: "Start time" }), createEl("th", { text: "Start time" }),
createEl("th", { text: "End time" }), createEl("th", { text: "End time" }),
createEl("th", { text: "Duration" }), createEl("th", { text: "Duration" }),
createEl("th")) createEl("th"));
for (let entry of orderedEntries(tracker.entries, settings)) for (let entry of orderedEntries(tracker.entries, settings))
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component) addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
// add copy buttons // add copy buttons
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" }) let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
new ButtonComponent(buttons) new ButtonComponent(buttons)
.setButtonText("Copy as table") .setButtonText("Copy as table")
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings))) .onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
new ButtonComponent(buttons) new ButtonComponent(buttons)
.setButtonText("Copy as CSV") .setButtonText("Copy as CSV")
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings))) .onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
} }
setCountdownValues(tracker, current, total, totalToday, currentDiv, settings) setCountdownValues(tracker, current, total, currentDiv, settings);
let intervalId = window.setInterval(() => { let intervalId = window.setInterval(() => {
// we delete the interval timer when the element is removed // we delete the interval timer when the element is removed
if (!element.isConnected) { if (!element.isConnected) {
window.clearInterval(intervalId) window.clearInterval(intervalId);
return return;
} }
setCountdownValues(tracker, current, total, totalToday, currentDiv, settings) setCountdownValues(tracker, current, total, currentDiv, settings);
}, 1000) }, 1000);
} }
export function getDuration(entry: Entry): number { export function getDuration(entry: Entry): number {
if (entry.subEntries) { if (entry.subEntries) {
return getTotalDuration(entry.subEntries) return getTotalDuration(entry.subEntries);
} else { } else {
let endTime = entry.endTime ? moment(entry.endTime) : moment() let endTime = entry.endTime ? moment(entry.endTime) : moment();
return endTime.diff(moment(entry.startTime)) return endTime.diff(moment(entry.startTime));
} }
} }
@ -169,10 +169,10 @@ export function getDurationToday(entry: Entry): number {
} }
export function getTotalDuration(entries: Entry[]): number { export function getTotalDuration(entries: Entry[]): number {
let ret = 0 let ret = 0;
for (let entry of entries) for (let entry of entries)
ret += getDuration(entry) ret += getDuration(entry);
return ret return ret;
} }
export function getTotalDurationToday(entries: Entry[]): number { export function getTotalDurationToday(entries: Entry[]): number {
@ -183,176 +183,175 @@ export function getTotalDurationToday(entries: Entry[]): number {
} }
export function isRunning(tracker: Tracker): boolean { export function isRunning(tracker: Tracker): boolean {
return !!getRunningEntry(tracker.entries) return !!getRunningEntry(tracker.entries);
} }
export function getRunningEntry(entries: Entry[]): Entry { export function getRunningEntry(entries: Entry[]): Entry {
for (let entry of entries) { for (let entry of entries) {
// if this entry has sub entries, check if one of them is running // if this entry has sub entries, check if one of them is running
if (entry.subEntries) { if (entry.subEntries) {
let running = getRunningEntry(entry.subEntries) let running = getRunningEntry(entry.subEntries);
if (running) if (running)
return running return running;
} else { } else {
// if this entry has no sub entries and no end time, it's running // if this entry has no sub entries and no end time, it's running
if (!entry.endTime) if (!entry.endTime)
return entry return entry;
} }
} }
return null return null;
} }
export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let table = [["Segment", "Start time", "End time", "Duration"]] let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of orderedEntries(tracker.entries, settings)) for (let entry of orderedEntries(tracker.entries, settings))
table.push(...createTableSection(entry, settings)) table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]) table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
let ret = "" let ret = "";
// calculate the width every column needs to look neat when monospaced // calculate the width every column needs to look neat when monospaced
let widths = Array.from(Array(4).keys()).map(i => Math.max(...table.map(a => a[i].length))) let widths = Array.from(Array(4).keys()).map(i => Math.max(...table.map(a => a[i].length)));
for (let r = 0; r < table.length; r++) { for (let r = 0; r < table.length; r++) {
// add separators after first row // add separators after first row
if (r == 1) if (r == 1)
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n" ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n";
let row: string[] = [] let row: string[] = [];
for (let i = 0; i < 4; i++) for (let i = 0; i < 4; i++)
row.push(table[r][i].padEnd(widths[i], " ")) row.push(table[r][i].padEnd(widths[i], " "));
ret += "| " + row.join(" | ") + " |\n" ret += "| " + row.join(" | ") + " |\n";
} }
return ret return ret;
} }
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "" let ret = "";
for (let entry of orderedEntries(tracker.entries, settings)) { for (let entry of orderedEntries(tracker.entries, settings)) {
for (let row of createTableSection(entry, settings)) for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n" ret += row.join(settings.csvDelimiter) + "\n";
} }
return ret return ret;
} }
export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] { export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
} }
export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string { export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment(timestamp).format(settings.timestampFormat) return moment(timestamp).format(settings.timestampFormat);
} }
export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string { export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
let ret = "" let ret = "";
let duration = moment.duration(totalTime) let duration = moment.duration(totalTime);
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours()) let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
if (settings.timestampDurations) { if (settings.timestampDurations) {
if (settings.fineGrainedDurations) { if (settings.fineGrainedDurations) {
let days = Math.floor(duration.asDays()) let days = Math.floor(duration.asDays());
if (days > 0) if (days > 0)
ret += days + "." ret += days + ".";
} }
ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}` ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`;
} else { } else {
if (settings.fineGrainedDurations) { if (settings.fineGrainedDurations) {
let years = Math.floor(duration.asYears()) let years = Math.floor(duration.asYears());
if (years > 0) if (years > 0)
ret += years + "y " ret += years + "y ";
if (duration.months() > 0) if (duration.months() > 0)
ret += duration.months() + "M " ret += duration.months() + "M ";
if (duration.days() > 0) if (duration.days() > 0)
ret += duration.days() + "d " ret += duration.days() + "d ";
} }
if (hours > 0) if (hours > 0)
ret += hours + "h " ret += hours + "h ";
if (duration.minutes() > 0) if (duration.minutes() > 0)
ret += duration.minutes() + "m " ret += duration.minutes() + "m ";
ret += duration.seconds() + "s" ret += duration.seconds() + "s";
} }
return ret return ret;
} }
function startSubEntry(entry: Entry, name: string): void { function startSubEntry(entry: Entry, name: string): void {
// if this entry is not split yet, we add its time as a sub-entry instead // if this entry is not split yet, we add its time as a sub-entry instead
if (!entry.subEntries) { if (!entry.subEntries) {
entry.subEntries = [{ ...entry, name: `Part 1` }] entry.subEntries = [{ ...entry, name: `Part 1` }];
entry.startTime = null entry.startTime = null;
entry.endTime = null entry.endTime = null;
} }
if (!name) if (!name)
name = `Part ${entry.subEntries.length + 1}` name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined }) entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined });
} }
function startNewEntry(tracker: Tracker, name: string): void { function startNewEntry(tracker: Tracker, name: string): void {
if (!name) if (!name)
name = `Segment ${tracker.entries.length + 1}` name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined } let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined };
tracker.entries.push(entry) tracker.entries.push(entry);
} }
function endRunningEntry(tracker: Tracker): void { function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries) let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().toISOString() entry.endTime = moment().toISOString();
} }
function removeEntry(entries: Entry[], toRemove: Entry): boolean { function removeEntry(entries: Entry[], toRemove: Entry): boolean {
if (entries.contains(toRemove)) { if (entries.contains(toRemove)) {
entries.remove(toRemove) entries.remove(toRemove);
return true return true;
} else { } else {
for (let entry of entries) { for (let entry of entries) {
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) { if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
// if we only have one sub entry remaining, we can merge back into our main entry // if we only have one sub entry remaining, we can merge back into our main entry
if (entry.subEntries.length == 1) { if (entry.subEntries.length == 1) {
let single = entry.subEntries[0] let single = entry.subEntries[0];
entry.startTime = single.startTime entry.startTime = single.startTime;
entry.endTime = single.endTime entry.endTime = single.endTime;
entry.subEntries = undefined entry.subEntries = undefined;
} }
return true return true;
} }
} }
} }
return false return false;
} }
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, totalToday: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void { function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
let running = getRunningEntry(tracker.entries) let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) { if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running), settings)) current.setText(formatDuration(getDuration(running), settings));
currentDiv.hidden = false currentDiv.hidden = false;
} else { } else {
currentDiv.hidden = true currentDiv.hidden = true;
} }
total.setText(formatDuration(getTotalDuration(tracker.entries), settings)) total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
totalToday.setText(formatDuration(getTotalDurationToday(tracker.entries), settings))
} }
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string { function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment(timestamp).format(settings.editableTimestampFormat) return moment(timestamp).format(settings.editableTimestampFormat);
} }
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string { function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
return moment(formatted, settings.editableTimestampFormat).toISOString() return moment(formatted, settings.editableTimestampFormat).toISOString();
} }
function updateLegacyInfo(entries: Entry[]): void { function updateLegacyInfo(entries: Entry[]): void {
for (let entry of entries) { for (let entry of entries) {
// in 0.1.8, timestamps were changed from unix to iso // in 0.1.8, timestamps were changed from unix to iso
if (entry.startTime && !isNaN(+entry.startTime)) if (entry.startTime && !isNaN(+entry.startTime))
entry.startTime = moment.unix(+entry.startTime).toISOString() entry.startTime = moment.unix(+entry.startTime).toISOString();
if (entry.endTime && !isNaN(+entry.endTime)) if (entry.endTime && !isNaN(+entry.endTime))
entry.endTime = moment.unix(+entry.endTime).toISOString() entry.endTime = moment.unix(+entry.endTime).toISOString();
// in 1.0.0, sub-entries were made optional // in 1.0.0, sub-entries were made optional
if (entry.subEntries == null || !entry.subEntries.length) if (entry.subEntries == null || !entry.subEntries.length)
entry.subEntries = undefined entry.subEntries = undefined;
if (entry.subEntries) if (entry.subEntries)
updateLegacyInfo(entry.subEntries) updateLegacyInfo(entry.subEntries);
} }
} }
@ -362,25 +361,25 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings):
entry.name, entry.name,
entry.startTime ? formatTimestamp(entry.startTime, settings) : "", entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
entry.endTime ? formatTimestamp(entry.endTime, settings) : "", entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]] entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]];
if (entry.subEntries) { if (entry.subEntries) {
for (let sub of orderedEntries(entry.subEntries, settings)) for (let sub of orderedEntries(entry.subEntries, settings))
ret.push(...createTableSection(sub, settings)) ret.push(...createTableSection(sub, settings));
} }
return ret return ret;
} }
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void { function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
let entryRunning = getRunningEntry(tracker.entries) == entry let entryRunning = getRunningEntry(tracker.entries) == entry;
let row = table.createEl("tr") let row = table.createEl("tr");
let nameField = new EditableField(row, indent, entry.name) let nameField = new EditableField(row, indent, entry.name);
let startField = new EditableTimestampField(row, (entry.startTime), settings) let startField = new EditableTimestampField(row, (entry.startTime), settings);
let endField = new EditableTimestampField(row, (entry.endTime), settings) let endField = new EditableTimestampField(row, (entry.endTime), settings);
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" }) row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" });
renderNameAsMarkdown(nameField.label, getFile, component) renderNameAsMarkdown(nameField.label, getFile, component);
let expandButton = new ButtonComponent(nameField.label) let expandButton = new ButtonComponent(nameField.label)
.setClass("clickable-icon") .setClass("clickable-icon")
@ -388,56 +387,56 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
.setIcon(`chevron-${entry.collapsed ? "left" : "down"}`) .setIcon(`chevron-${entry.collapsed ? "left" : "down"}`)
.onClick(async () => { .onClick(async () => {
if (entry.collapsed) { if (entry.collapsed) {
entry.collapsed = undefined entry.collapsed = undefined;
} else { } else {
entry.collapsed = true entry.collapsed = true;
} }
await saveTracker(tracker, getFile(), getSectionInfo()) await saveTracker(tracker, getFile(), getSectionInfo());
}) });
if (!entry.subEntries) if (!entry.subEntries)
expandButton.buttonEl.style.visibility = "hidden" expandButton.buttonEl.style.visibility = "hidden";
let entryButtons = row.createEl("td") let entryButtons = row.createEl("td");
entryButtons.addClass("simple-time-tracker-table-buttons") entryButtons.addClass("simple-time-tracker-table-buttons");
new ButtonComponent(entryButtons) new ButtonComponent(entryButtons)
.setClass("clickable-icon") .setClass("clickable-icon")
.setIcon(`lucide-play`) .setIcon(`lucide-play`)
.setTooltip("Continue") .setTooltip("Continue")
.setDisabled(trackerRunning) .setDisabled(trackerRunning)
.onClick(async () => { .onClick(async () => {
startSubEntry(entry, newSegmentNameBox.getValue()) startSubEntry(entry, newSegmentNameBox.getValue());
await saveTracker(tracker, getFile(), getSectionInfo()) await saveTracker(tracker, getFile(), getSectionInfo());
}) });
let editButton = new ButtonComponent(entryButtons) let editButton = new ButtonComponent(entryButtons)
.setClass("clickable-icon") .setClass("clickable-icon")
.setTooltip("Edit") .setTooltip("Edit")
.setIcon("lucide-pencil") .setIcon("lucide-pencil")
.onClick(async () => { .onClick(async () => {
if (nameField.editing()) { if (nameField.editing()) {
entry.name = nameField.endEdit() entry.name = nameField.endEdit();
expandButton.buttonEl.style.display = null expandButton.buttonEl.style.display = null;
startField.endEdit() startField.endEdit();
entry.startTime = startField.getTimestamp() entry.startTime = startField.getTimestamp();
if (!entryRunning) { if (!entryRunning) {
endField.endEdit() endField.endEdit();
entry.endTime = endField.getTimestamp() entry.endTime = endField.getTimestamp();
} }
await saveTracker(tracker, getFile(), getSectionInfo()) await saveTracker(tracker, getFile(), getSectionInfo());
editButton.setIcon("lucide-pencil") editButton.setIcon("lucide-pencil");
renderNameAsMarkdown(nameField.label, getFile, component) renderNameAsMarkdown(nameField.label, getFile, component);
} else { } else {
nameField.beginEdit(entry.name) nameField.beginEdit(entry.name);
expandButton.buttonEl.style.display = "none" expandButton.buttonEl.style.display = "none";
// only allow editing start and end times if we don't have sub entries // only allow editing start and end times if we don't have sub entries
if (!entry.subEntries) { if (!entry.subEntries) {
startField.beginEdit(entry.startTime) startField.beginEdit(entry.startTime);
if (!entryRunning) if (!entryRunning)
endField.beginEdit(entry.endTime) endField.beginEdit(entry.endTime);
} }
editButton.setIcon("lucide-check") editButton.setIcon("lucide-check");
} }
}) });
new ButtonComponent(entryButtons) new ButtonComponent(entryButtons)
.setClass("clickable-icon") .setClass("clickable-icon")
.setTooltip("Remove") .setTooltip("Remove")
@ -445,100 +444,100 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
.setDisabled(entryRunning) .setDisabled(entryRunning)
.onClick(async () => { .onClick(async () => {
const confirmed = await showConfirm("Are you sure you want to delete this entry?") const confirmed = await showConfirm("Are you sure you want to delete this entry?");
if (!confirmed) { if (!confirmed) {
return return;
} }
removeEntry(tracker.entries, entry) removeEntry(tracker.entries, entry);
await saveTracker(tracker, getFile(), getSectionInfo()) await saveTracker(tracker, getFile(), getSectionInfo());
}) });
if (entry.subEntries && !entry.collapsed) { if (entry.subEntries && !entry.collapsed) {
for (let sub of orderedEntries(entry.subEntries, settings)) for (let sub of orderedEntries(entry.subEntries, settings))
addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component) addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
} }
} }
function showConfirm(message: string): Promise<boolean> { function showConfirm(message: string): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
const modal = new ConfirmModal(app, message, resolve) const modal = new ConfirmModal(app, message, resolve);
modal.open() modal.open();
}) });
} }
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void { function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
// we don't have to wait here since async code only occurs when a file needs to be loaded (like a linked image) // we don't have to wait here since async code only occurs when a file needs to be loaded (like a linked image)
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component) void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
// rendering wraps it in a paragraph // rendering wraps it in a paragraph
label.innerHTML = label.querySelector("p").innerHTML label.innerHTML = label.querySelector("p").innerHTML;
} }
class EditableField { class EditableField {
cell: HTMLTableCellElement cell: HTMLTableCellElement;
label: HTMLSpanElement label: HTMLSpanElement;
box: TextComponent box: TextComponent;
constructor(row: HTMLTableRowElement, indent: number, value: string) { constructor(row: HTMLTableRowElement, indent: number, value: string) {
this.cell = row.createEl("td") this.cell = row.createEl("td");
this.label = this.cell.createEl("span", { text: value }) this.label = this.cell.createEl("span", { text: value });
this.label.style.marginLeft = `${indent}em` this.label.style.marginLeft = `${indent}em`;
this.box = new TextComponent(this.cell).setValue(value) this.box = new TextComponent(this.cell).setValue(value);
this.box.inputEl.addClass("simple-time-tracker-input") this.box.inputEl.addClass("simple-time-tracker-input");
this.box.inputEl.hide() this.box.inputEl.hide();
} }
editing(): boolean { editing(): boolean {
return this.label.hidden return this.label.hidden;
} }
beginEdit(value: string): void { beginEdit(value: string): void {
this.label.hidden = true this.label.hidden = true;
this.box.setValue(value) this.box.setValue(value);
this.box.inputEl.show() this.box.inputEl.show();
} }
endEdit(): string { endEdit(): string {
const value = this.box.getValue() const value = this.box.getValue();
this.label.setText(value) this.label.setText(value);
this.box.inputEl.hide() this.box.inputEl.hide();
this.label.hidden = false this.label.hidden = false;
return value return value;
} }
} }
class EditableTimestampField extends EditableField { class EditableTimestampField extends EditableField {
settings: SimpleTimeTrackerSettings settings: SimpleTimeTrackerSettings;
constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) { constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) {
super(row, 0, value ? formatTimestamp(value, settings) : "") super(row, 0, value ? formatTimestamp(value, settings) : "");
this.settings = settings this.settings = settings;
} }
beginEdit(value: string): void { beginEdit(value: string): void {
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "") super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "");
} }
endEdit(): string { endEdit(): string {
const value = this.box.getValue() const value = this.box.getValue();
let displayValue = value let displayValue = value;
if (value) { if (value) {
const timestamp = unformatEditableTimestamp(value, this.settings) const timestamp = unformatEditableTimestamp(value, this.settings);
displayValue = formatTimestamp(timestamp, this.settings) displayValue = formatTimestamp(timestamp, this.settings);
} }
this.label.setText(displayValue) this.label.setText(displayValue);
this.box.inputEl.hide() this.box.inputEl.hide();
this.label.hidden = false this.label.hidden = false;
return value return value;
} }
getTimestamp(): string { getTimestamp(): string {
if (this.box.getValue()) { if (this.box.getValue()) {
return unformatEditableTimestamp(this.box.getValue(), this.settings) return unformatEditableTimestamp(this.box.getValue(), this.settings);
} else { } else {
return null return null;
} }
} }
} }