This commit is contained in:
hen 2024-11-14 00:53:38 -03:00 committed by GitHub
commit 094c53e4f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,327 +1,358 @@
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 totalToday = totalTodayDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" })
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, currentDiv, settings); setCountdownValues(tracker, current, total, totalToday, 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, currentDiv, settings); setCountdownValues(tracker, current, total, totalToday, 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))
}
}
export function getDurationToday(entry: Entry): number {
if (entry.subEntries) {
return getTotalDurationToday(entry.subEntries)
} else {
let today = moment().startOf('day')
let endTime = entry.endTime ? moment(entry.endTime) : moment()
let startTime = moment(entry.startTime)
if (endTime.isBefore(today)) {
return 0
}
if (startTime.isBefore(today)) {
startTime = today
}
return endTime.diff(startTime)
} }
} }
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 {
let ret = 0
for (let entry of entries)
ret += getDurationToday(entry)
return ret
} }
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, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void { function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, totalToday: 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)
} }
} }
@ -331,25 +362,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")
@ -357,56 +388,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")
@ -414,100 +445,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
} }
} }
} }