mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-12-23 05:39:23 +01:00
add total today
This commit is contained in:
parent
7033f28555
commit
d4188127b3
1 changed files with 241 additions and 210 deletions
451
src/tracker.ts
451
src/tracker.ts
|
@ -1,327 +1,358 @@
|
|||
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian";
|
||||
import { SimpleTimeTrackerSettings } from "./settings";
|
||||
import { ConfirmModal } from "./confirm-modal";
|
||||
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian"
|
||||
import { SimpleTimeTrackerSettings } from "./settings"
|
||||
import { ConfirmModal } from "./confirm-modal"
|
||||
|
||||
export interface Tracker {
|
||||
entries: Entry[];
|
||||
entries: Entry[]
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
name: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
subEntries?: Entry[];
|
||||
collapsed?: boolean;
|
||||
name: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
subEntries?: Entry[]
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
let content = await app.vault.read(file);
|
||||
return
|
||||
let content = await app.vault.read(file)
|
||||
|
||||
// figure out what part of the content we have to edit
|
||||
let lines = content.split("\n");
|
||||
let prev = lines.filter((_, i) => i <= section.lineStart).join("\n");
|
||||
let next = lines.filter((_, i) => i >= section.lineEnd).join("\n");
|
||||
let lines = content.split("\n")
|
||||
let prev = lines.filter((_, i) => i <= section.lineStart).join("\n")
|
||||
let next = lines.filter((_, i) => i >= section.lineEnd).join("\n")
|
||||
// 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 {
|
||||
if (json) {
|
||||
try {
|
||||
let ret = JSON.parse(json);
|
||||
updateLegacyInfo(ret.entries);
|
||||
return ret;
|
||||
let ret = JSON.parse(json)
|
||||
updateLegacyInfo(ret.entries)
|
||||
return ret
|
||||
} 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 }[]> {
|
||||
let file = app.vault.getAbstractFileByPath(fileName);
|
||||
let content = (await app.vault.cachedRead(file as TFile)).split("\n");
|
||||
let file = app.vault.getAbstractFileByPath(fileName)
|
||||
let content = (await app.vault.cachedRead(file as TFile)).split("\n")
|
||||
|
||||
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = [];
|
||||
let curr: Partial<MarkdownSectionInformation> | undefined;
|
||||
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = []
|
||||
let curr: Partial<MarkdownSectionInformation> | undefined
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let line = content[i];
|
||||
let line = content[i]
|
||||
if (line.trimEnd() == "```simple-time-tracker") {
|
||||
curr = { lineStart: i + 1, text: "" };
|
||||
curr = { lineStart: i + 1, text: "" }
|
||||
} else if (curr) {
|
||||
if (line.trimEnd() == "```") {
|
||||
curr.lineEnd = i - 1;
|
||||
let tracker = loadTracker(curr.text);
|
||||
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker });
|
||||
curr = undefined;
|
||||
curr.lineEnd = i - 1
|
||||
let tracker = loadTracker(curr.text)
|
||||
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker })
|
||||
curr = undefined
|
||||
} 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 {
|
||||
|
||||
element.addClass("simple-time-tracker-container");
|
||||
element.addClass("simple-time-tracker-container")
|
||||
// add start/stop controls
|
||||
let running = isRunning(tracker);
|
||||
let running = isRunning(tracker)
|
||||
let btn = new ButtonComponent(element)
|
||||
.setClass("clickable-icon")
|
||||
.setIcon(`lucide-${running ? "stop" : "play"}-circle`)
|
||||
.setTooltip(running ? "End" : "Start")
|
||||
.onClick(async () => {
|
||||
if (running) {
|
||||
endRunningEntry(tracker);
|
||||
endRunningEntry(tracker)
|
||||
} else {
|
||||
startNewEntry(tracker, newSegmentNameBox.getValue());
|
||||
startNewEntry(tracker, newSegmentNameBox.getValue())
|
||||
}
|
||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
||||
});
|
||||
btn.buttonEl.addClass("simple-time-tracker-btn");
|
||||
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||
})
|
||||
btn.buttonEl.addClass("simple-time-tracker-btn")
|
||||
let newSegmentNameBox = new TextComponent(element)
|
||||
.setPlaceholder("Segment name")
|
||||
.setDisabled(running);
|
||||
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
|
||||
.setDisabled(running)
|
||||
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt")
|
||||
|
||||
// add timers
|
||||
let timer = element.createDiv({ cls: "simple-time-tracker-timers" });
|
||||
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
||||
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" });
|
||||
currentDiv.createEl("span", { text: "Current" });
|
||||
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
||||
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" });
|
||||
totalDiv.createEl("span", { text: "Total" });
|
||||
let timer = element.createDiv({ cls: "simple-time-tracker-timers" })
|
||||
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
|
||||
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" })
|
||||
currentDiv.createEl("span", { text: "Current" })
|
||||
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
|
||||
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" })
|
||||
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) {
|
||||
// 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(
|
||||
createEl("th", { text: "Segment" }),
|
||||
createEl("th", { text: "Start time" }),
|
||||
createEl("th", { text: "End time" }),
|
||||
createEl("th", { text: "Duration" }),
|
||||
createEl("th"));
|
||||
createEl("th"))
|
||||
|
||||
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
|
||||
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
|
||||
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" })
|
||||
new ButtonComponent(buttons)
|
||||
.setButtonText("Copy as table")
|
||||
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
|
||||
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)))
|
||||
new ButtonComponent(buttons)
|
||||
.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(() => {
|
||||
// we delete the interval timer when the element is removed
|
||||
if (!element.isConnected) {
|
||||
window.clearInterval(intervalId);
|
||||
return;
|
||||
window.clearInterval(intervalId)
|
||||
return
|
||||
}
|
||||
setCountdownValues(tracker, current, total, currentDiv, settings);
|
||||
}, 1000);
|
||||
setCountdownValues(tracker, current, total, totalToday, currentDiv, settings)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
export function getDuration(entry: Entry): number {
|
||||
if (entry.subEntries) {
|
||||
return getTotalDuration(entry.subEntries);
|
||||
return getTotalDuration(entry.subEntries)
|
||||
} else {
|
||||
let endTime = entry.endTime ? moment(entry.endTime) : moment();
|
||||
return endTime.diff(moment(entry.startTime));
|
||||
let endTime = entry.endTime ? moment(entry.endTime) : moment()
|
||||
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 {
|
||||
let ret = 0;
|
||||
let ret = 0
|
||||
for (let entry of entries)
|
||||
ret += getDuration(entry);
|
||||
return ret;
|
||||
ret += getDuration(entry)
|
||||
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 {
|
||||
return !!getRunningEntry(tracker.entries);
|
||||
return !!getRunningEntry(tracker.entries)
|
||||
}
|
||||
|
||||
export function getRunningEntry(entries: Entry[]): Entry {
|
||||
for (let entry of entries) {
|
||||
// if this entry has sub entries, check if one of them is running
|
||||
if (entry.subEntries) {
|
||||
let running = getRunningEntry(entry.subEntries);
|
||||
let running = getRunningEntry(entry.subEntries)
|
||||
if (running)
|
||||
return running;
|
||||
return running
|
||||
} else {
|
||||
// if this entry has no sub entries and no end time, it's running
|
||||
if (!entry.endTime)
|
||||
return entry;
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
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))
|
||||
table.push(...createTableSection(entry, settings));
|
||||
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
|
||||
table.push(...createTableSection(entry, settings))
|
||||
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`])
|
||||
|
||||
let ret = "";
|
||||
let ret = ""
|
||||
// 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++) {
|
||||
// add separators after first row
|
||||
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++)
|
||||
row.push(table[r][i].padEnd(widths[i], " "));
|
||||
ret += "| " + row.join(" | ") + " |\n";
|
||||
row.push(table[r][i].padEnd(widths[i], " "))
|
||||
ret += "| " + row.join(" | ") + " |\n"
|
||||
}
|
||||
return ret;
|
||||
return ret
|
||||
}
|
||||
|
||||
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
||||
let ret = "";
|
||||
let ret = ""
|
||||
for (let entry of orderedEntries(tracker.entries, 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[] {
|
||||
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
|
||||
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries
|
||||
}
|
||||
|
||||
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 {
|
||||
let ret = "";
|
||||
let duration = moment.duration(totalTime);
|
||||
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
|
||||
let ret = ""
|
||||
let duration = moment.duration(totalTime)
|
||||
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours())
|
||||
|
||||
if (settings.timestampDurations) {
|
||||
if (settings.fineGrainedDurations) {
|
||||
let days = Math.floor(duration.asDays());
|
||||
let days = Math.floor(duration.asDays())
|
||||
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 {
|
||||
if (settings.fineGrainedDurations) {
|
||||
let years = Math.floor(duration.asYears());
|
||||
let years = Math.floor(duration.asYears())
|
||||
if (years > 0)
|
||||
ret += years + "y ";
|
||||
ret += years + "y "
|
||||
if (duration.months() > 0)
|
||||
ret += duration.months() + "M ";
|
||||
ret += duration.months() + "M "
|
||||
if (duration.days() > 0)
|
||||
ret += duration.days() + "d ";
|
||||
ret += duration.days() + "d "
|
||||
}
|
||||
if (hours > 0)
|
||||
ret += hours + "h ";
|
||||
ret += hours + "h "
|
||||
if (duration.minutes() > 0)
|
||||
ret += duration.minutes() + "m ";
|
||||
ret += duration.seconds() + "s";
|
||||
ret += duration.minutes() + "m "
|
||||
ret += duration.seconds() + "s"
|
||||
}
|
||||
return ret;
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
function startSubEntry(entry: Entry, name: string): void {
|
||||
// if this entry is not split yet, we add its time as a sub-entry instead
|
||||
if (!entry.subEntries) {
|
||||
entry.subEntries = [{ ...entry, name: `Part 1` }];
|
||||
entry.startTime = null;
|
||||
entry.endTime = null;
|
||||
entry.subEntries = [{ ...entry, name: `Part 1` }]
|
||||
entry.startTime = null
|
||||
entry.endTime = null
|
||||
}
|
||||
|
||||
if (!name)
|
||||
name = `Part ${entry.subEntries.length + 1}`;
|
||||
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined });
|
||||
name = `Part ${entry.subEntries.length + 1}`
|
||||
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined })
|
||||
}
|
||||
|
||||
function startNewEntry(tracker: Tracker, name: string): void {
|
||||
if (!name)
|
||||
name = `Segment ${tracker.entries.length + 1}`;
|
||||
let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined };
|
||||
tracker.entries.push(entry);
|
||||
name = `Segment ${tracker.entries.length + 1}`
|
||||
let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined }
|
||||
tracker.entries.push(entry)
|
||||
}
|
||||
|
||||
function endRunningEntry(tracker: Tracker): void {
|
||||
let entry = getRunningEntry(tracker.entries);
|
||||
entry.endTime = moment().toISOString();
|
||||
let entry = getRunningEntry(tracker.entries)
|
||||
entry.endTime = moment().toISOString()
|
||||
}
|
||||
|
||||
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
|
||||
if (entries.contains(toRemove)) {
|
||||
entries.remove(toRemove);
|
||||
return true;
|
||||
entries.remove(toRemove)
|
||||
return true
|
||||
} else {
|
||||
for (let entry of entries) {
|
||||
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
|
||||
// if we only have one sub entry remaining, we can merge back into our main entry
|
||||
if (entry.subEntries.length == 1) {
|
||||
let single = entry.subEntries[0];
|
||||
entry.startTime = single.startTime;
|
||||
entry.endTime = single.endTime;
|
||||
entry.subEntries = undefined;
|
||||
let single = entry.subEntries[0]
|
||||
entry.startTime = single.startTime
|
||||
entry.endTime = single.endTime
|
||||
entry.subEntries = undefined
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
||||
let running = getRunningEntry(tracker.entries);
|
||||
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, totalToday: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
||||
let running = getRunningEntry(tracker.entries)
|
||||
if (running && !running.endTime) {
|
||||
current.setText(formatDuration(getDuration(running), settings));
|
||||
currentDiv.hidden = false;
|
||||
current.setText(formatDuration(getDuration(running), settings))
|
||||
currentDiv.hidden = false
|
||||
} 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 {
|
||||
return moment(timestamp).format(settings.editableTimestampFormat);
|
||||
return moment(timestamp).format(settings.editableTimestampFormat)
|
||||
}
|
||||
|
||||
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
|
||||
return moment(formatted, settings.editableTimestampFormat).toISOString();
|
||||
return moment(formatted, settings.editableTimestampFormat).toISOString()
|
||||
}
|
||||
|
||||
function updateLegacyInfo(entries: Entry[]): void {
|
||||
for (let entry of entries) {
|
||||
// in 0.1.8, timestamps were changed from unix to iso
|
||||
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))
|
||||
entry.endTime = moment.unix(+entry.endTime).toISOString();
|
||||
entry.endTime = moment.unix(+entry.endTime).toISOString()
|
||||
|
||||
// in 1.0.0, sub-entries were made optional
|
||||
if (entry.subEntries == null || !entry.subEntries.length)
|
||||
entry.subEntries = undefined;
|
||||
entry.subEntries = undefined
|
||||
|
||||
if (entry.subEntries)
|
||||
updateLegacyInfo(entry.subEntries);
|
||||
updateLegacyInfo(entry.subEntries)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,25 +362,25 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings):
|
|||
entry.name,
|
||||
entry.startTime ? formatTimestamp(entry.startTime, 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) {
|
||||
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 {
|
||||
let entryRunning = getRunningEntry(tracker.entries) == entry;
|
||||
let row = table.createEl("tr");
|
||||
let entryRunning = getRunningEntry(tracker.entries) == entry
|
||||
let row = table.createEl("tr")
|
||||
|
||||
let nameField = new EditableField(row, indent, entry.name);
|
||||
let startField = new EditableTimestampField(row, (entry.startTime), settings);
|
||||
let endField = new EditableTimestampField(row, (entry.endTime), settings);
|
||||
let nameField = new EditableField(row, indent, entry.name)
|
||||
let startField = new EditableTimestampField(row, (entry.startTime), 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)
|
||||
.setClass("clickable-icon")
|
||||
|
@ -357,56 +388,56 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
|||
.setIcon(`chevron-${entry.collapsed ? "left" : "down"}`)
|
||||
.onClick(async () => {
|
||||
if (entry.collapsed) {
|
||||
entry.collapsed = undefined;
|
||||
entry.collapsed = undefined
|
||||
} else {
|
||||
entry.collapsed = true;
|
||||
entry.collapsed = true
|
||||
}
|
||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
||||
});
|
||||
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||
})
|
||||
if (!entry.subEntries)
|
||||
expandButton.buttonEl.style.visibility = "hidden";
|
||||
expandButton.buttonEl.style.visibility = "hidden"
|
||||
|
||||
let entryButtons = row.createEl("td");
|
||||
entryButtons.addClass("simple-time-tracker-table-buttons");
|
||||
let entryButtons = row.createEl("td")
|
||||
entryButtons.addClass("simple-time-tracker-table-buttons")
|
||||
new ButtonComponent(entryButtons)
|
||||
.setClass("clickable-icon")
|
||||
.setIcon(`lucide-play`)
|
||||
.setTooltip("Continue")
|
||||
.setDisabled(trackerRunning)
|
||||
.onClick(async () => {
|
||||
startSubEntry(entry, newSegmentNameBox.getValue());
|
||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
||||
});
|
||||
startSubEntry(entry, newSegmentNameBox.getValue())
|
||||
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||
})
|
||||
let editButton = new ButtonComponent(entryButtons)
|
||||
.setClass("clickable-icon")
|
||||
.setTooltip("Edit")
|
||||
.setIcon("lucide-pencil")
|
||||
.onClick(async () => {
|
||||
if (nameField.editing()) {
|
||||
entry.name = nameField.endEdit();
|
||||
expandButton.buttonEl.style.display = null;
|
||||
startField.endEdit();
|
||||
entry.startTime = startField.getTimestamp();
|
||||
entry.name = nameField.endEdit()
|
||||
expandButton.buttonEl.style.display = null
|
||||
startField.endEdit()
|
||||
entry.startTime = startField.getTimestamp()
|
||||
if (!entryRunning) {
|
||||
endField.endEdit();
|
||||
entry.endTime = endField.getTimestamp();
|
||||
endField.endEdit()
|
||||
entry.endTime = endField.getTimestamp()
|
||||
}
|
||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
||||
editButton.setIcon("lucide-pencil");
|
||||
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||
editButton.setIcon("lucide-pencil")
|
||||
|
||||
renderNameAsMarkdown(nameField.label, getFile, component);
|
||||
renderNameAsMarkdown(nameField.label, getFile, component)
|
||||
} else {
|
||||
nameField.beginEdit(entry.name);
|
||||
expandButton.buttonEl.style.display = "none";
|
||||
nameField.beginEdit(entry.name)
|
||||
expandButton.buttonEl.style.display = "none"
|
||||
// only allow editing start and end times if we don't have sub entries
|
||||
if (!entry.subEntries) {
|
||||
startField.beginEdit(entry.startTime);
|
||||
startField.beginEdit(entry.startTime)
|
||||
if (!entryRunning)
|
||||
endField.beginEdit(entry.endTime);
|
||||
endField.beginEdit(entry.endTime)
|
||||
}
|
||||
editButton.setIcon("lucide-check");
|
||||
editButton.setIcon("lucide-check")
|
||||
}
|
||||
});
|
||||
})
|
||||
new ButtonComponent(entryButtons)
|
||||
.setClass("clickable-icon")
|
||||
.setTooltip("Remove")
|
||||
|
@ -414,100 +445,100 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
|||
.setDisabled(entryRunning)
|
||||
.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) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
removeEntry(tracker.entries, entry);
|
||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
||||
});
|
||||
removeEntry(tracker.entries, entry)
|
||||
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||
})
|
||||
|
||||
if (entry.subEntries && !entry.collapsed) {
|
||||
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> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ConfirmModal(app, message, resolve);
|
||||
modal.open();
|
||||
});
|
||||
const modal = new ConfirmModal(app, message, resolve)
|
||||
modal.open()
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
|
||||
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component)
|
||||
// rendering wraps it in a paragraph
|
||||
label.innerHTML = label.querySelector("p").innerHTML;
|
||||
label.innerHTML = label.querySelector("p").innerHTML
|
||||
}
|
||||
|
||||
|
||||
class EditableField {
|
||||
cell: HTMLTableCellElement;
|
||||
label: HTMLSpanElement;
|
||||
box: TextComponent;
|
||||
cell: HTMLTableCellElement
|
||||
label: HTMLSpanElement
|
||||
box: TextComponent
|
||||
|
||||
constructor(row: HTMLTableRowElement, indent: number, value: string) {
|
||||
this.cell = row.createEl("td");
|
||||
this.label = this.cell.createEl("span", { text: value });
|
||||
this.label.style.marginLeft = `${indent}em`;
|
||||
this.box = new TextComponent(this.cell).setValue(value);
|
||||
this.box.inputEl.addClass("simple-time-tracker-input");
|
||||
this.box.inputEl.hide();
|
||||
this.cell = row.createEl("td")
|
||||
this.label = this.cell.createEl("span", { text: value })
|
||||
this.label.style.marginLeft = `${indent}em`
|
||||
this.box = new TextComponent(this.cell).setValue(value)
|
||||
this.box.inputEl.addClass("simple-time-tracker-input")
|
||||
this.box.inputEl.hide()
|
||||
}
|
||||
|
||||
editing(): boolean {
|
||||
return this.label.hidden;
|
||||
return this.label.hidden
|
||||
}
|
||||
|
||||
beginEdit(value: string): void {
|
||||
this.label.hidden = true;
|
||||
this.box.setValue(value);
|
||||
this.box.inputEl.show();
|
||||
this.label.hidden = true
|
||||
this.box.setValue(value)
|
||||
this.box.inputEl.show()
|
||||
}
|
||||
|
||||
endEdit(): string {
|
||||
const value = this.box.getValue();
|
||||
this.label.setText(value);
|
||||
this.box.inputEl.hide();
|
||||
this.label.hidden = false;
|
||||
return value;
|
||||
const value = this.box.getValue()
|
||||
this.label.setText(value)
|
||||
this.box.inputEl.hide()
|
||||
this.label.hidden = false
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class EditableTimestampField extends EditableField {
|
||||
settings: SimpleTimeTrackerSettings;
|
||||
settings: SimpleTimeTrackerSettings
|
||||
|
||||
constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) {
|
||||
super(row, 0, value ? formatTimestamp(value, settings) : "");
|
||||
this.settings = settings;
|
||||
super(row, 0, value ? formatTimestamp(value, settings) : "")
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
beginEdit(value: string): void {
|
||||
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "");
|
||||
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "")
|
||||
}
|
||||
|
||||
endEdit(): string {
|
||||
const value = this.box.getValue();
|
||||
let displayValue = value;
|
||||
const value = this.box.getValue()
|
||||
let displayValue = value
|
||||
if (value) {
|
||||
const timestamp = unformatEditableTimestamp(value, this.settings);
|
||||
displayValue = formatTimestamp(timestamp, this.settings);
|
||||
const timestamp = unformatEditableTimestamp(value, this.settings)
|
||||
displayValue = formatTimestamp(timestamp, this.settings)
|
||||
}
|
||||
this.label.setText(displayValue);
|
||||
this.box.inputEl.hide();
|
||||
this.label.hidden = false;
|
||||
return value;
|
||||
this.label.setText(displayValue)
|
||||
this.box.inputEl.hide()
|
||||
this.label.hidden = false
|
||||
return value
|
||||
}
|
||||
|
||||
getTimestamp(): string {
|
||||
if (this.box.getValue()) {
|
||||
return unformatEditableTimestamp(this.box.getValue(), this.settings);
|
||||
return unformatEditableTimestamp(this.box.getValue(), this.settings)
|
||||
} else {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue