ObsidianSimpleTimeTracker/src/tracker.ts

238 lines
9.2 KiB
TypeScript
Raw Normal View History

2022-09-27 21:44:28 +02:00
import { moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent } from "obsidian";
2022-09-28 13:18:37 +02:00
import { SimpleTimeTrackerSettings } from "./settings";
2022-09-27 16:06:40 +02:00
2022-09-29 16:01:20 +02:00
export interface Tracker {
2022-09-27 17:03:44 +02:00
entries: Entry[];
}
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export interface Entry {
name: string;
startTime: number;
endTime: number;
}
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export function startEntry(tracker: Tracker, name: string): void {
2022-09-27 21:23:36 +02:00
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = { name: name, startTime: moment().unix(), endTime: null };
2022-09-27 17:03:44 +02:00
tracker.entries.push(entry);
};
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export function endEntry(tracker: Tracker): void {
let last = tracker.entries.last();
2022-09-27 21:23:36 +02:00
last.endTime = moment().unix();
2022-09-27 17:03:44 +02:00
}
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export function isRunning(tracker: Tracker): boolean {
let last = tracker.entries.last();
return last != null && !last.endTime;
}
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export async function saveTracker(tracker: Tracker, app: App, section: MarkdownSectionInformation): Promise<void> {
let file = app.workspace.getActiveFile();
2022-09-29 16:05:39 +02:00
if (!file)
return;
let content = await app.vault.read(file);
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
// 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");
// edit only the code block content, leave the rest untouched
content = `${prev}\n${JSON.stringify(tracker)}\n${next}`;
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
await app.vault.modify(file, content);
}
2022-09-27 16:06:40 +02:00
2022-09-27 17:03:44 +02:00
export function loadTracker(json: string): Tracker {
if (json) {
try {
return JSON.parse(json);
} catch (e) {
console.log(`Failed to parse Tracker from ${json}`);
2022-09-27 16:06:40 +02:00
}
}
2022-09-27 17:03:44 +02:00
return { entries: [] };
2022-09-27 16:06:40 +02:00
}
2022-09-28 13:18:37 +02:00
export function displayTracker(tracker: Tracker, element: HTMLElement, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings): void {
2022-09-27 21:44:28 +02:00
// add start/stop controls
let running = isRunning(tracker);
let btn = new ButtonComponent(element)
2022-10-10 14:51:11 +02:00
.setClass("clickable-icon")
.setIcon(`lucide-${running ? "stop" : "play"}-circle`)
.setTooltip(running ? "End" : "Start")
2022-09-27 21:44:28 +02:00
.onClick(async () => {
if (running) {
endEntry(tracker);
} else {
startEntry(tracker, name.getValue());
}
await saveTracker(tracker, this.app, getSectionInfo());
});
btn.buttonEl.addClass("simple-time-tracker-btn");
let name = new TextComponent(element)
.setPlaceholder("Segment name")
.setDisabled(running);
name.inputEl.addClass("simple-time-tracker-txt");
2022-09-27 21:23:36 +02:00
// add timers
2022-09-27 19:33:34 +02:00
let timer = element.createDiv({ cls: "simple-time-tracker-timers" });
2022-09-27 21:23:36 +02:00
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
2022-09-27 21:35:26 +02:00
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" });
2022-09-27 21:40:32 +02:00
currentDiv.createEl("span", { text: "Current" });
2022-09-27 21:23:36 +02:00
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
2022-09-27 21:52:02 +02:00
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" });
2022-09-27 21:40:32 +02:00
totalDiv.createEl("span", { text: "Total" });
2022-09-27 21:23:36 +02:00
2022-09-27 21:52:02 +02:00
if (tracker.entries.length > 0) {
// add table
2022-09-27 21:52:02 +02:00
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"));
2022-09-27 21:52:02 +02:00
for (let entry of tracker.entries) {
let row = table.createEl("tr");
let name = row.createEl("td");
let namePar = name.createEl("span", { text: entry.name });
let nameBox = new TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", { text: formatTimestamp(entry.startTime, settings) });
2022-10-10 14:51:11 +02:00
row.createEl("td", { text: entry.endTime ? formatTimestamp(entry.endTime, settings) : "" });
row.createEl("td", { text: entry.endTime ? formatDurationBetween(entry.startTime, entry.endTime) : "" });
let entryButtons = row.createEl("td");
let editButton = new ButtonComponent(entryButtons)
.setClass("clickable-icon")
.setTooltip("Edit")
.setIcon("lucide-pencil")
.onClick(async () => {
if (namePar.hidden) {
namePar.hidden = false;
nameBox.inputEl.hidden = true;
editButton.setIcon("lucide-pencil");
if (nameBox.getValue()) {
entry.name = nameBox.getValue();
namePar.setText(entry.name);
await saveTracker(tracker, this.app, getSectionInfo());
}
} else {
namePar.hidden = true;
nameBox.inputEl.hidden = false;
nameBox.setValue(entry.name);
editButton.setIcon("lucide-check");
}
});
new ButtonComponent(entryButtons)
.setClass("clickable-icon")
.setTooltip("Remove")
.setIcon("lucide-trash")
.onClick(async () => {
tracker.entries.remove(entry);
await saveTracker(tracker, this.app, getSectionInfo());
});
2022-09-27 21:23:36 +02:00
}
// add copy buttons
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
new ButtonComponent(buttons)
.setButtonText("Copy as table")
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
new ButtonComponent(buttons)
2022-09-28 14:11:49 +02:00
.setButtonText("Copy as CSV")
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
2022-09-27 21:23:36 +02:00
}
2022-09-27 21:23:36 +02:00
setCountdownValues(tracker, current, total, currentDiv);
let intervalId = window.setInterval(() => {
// we delete the interval timer when the element is removed
if (!element.isConnected) {
window.clearInterval(intervalId);
return;
}
setCountdownValues(tracker, current, total, currentDiv);
}, 1000);
2022-09-28 00:52:52 +02:00
}
2022-09-27 16:06:40 +02:00
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement) {
let currEntry = tracker.entries.last();
if (currEntry) {
if (!currEntry.endTime)
current.setText(formatDurationBetween(currEntry.startTime, moment().unix()));
total.setText(formatDuration(getTotalDuration(tracker)));
}
currentDiv.hidden = !currEntry || !!currEntry.endTime;
}
function getTotalDuration(tracker: Tracker): number {
let totalDuration = 0;
for (let entry of tracker.entries) {
let endTime = entry.endTime ? moment.unix(entry.endTime) : moment();
totalDuration += endTime.diff(moment.unix(entry.startTime));
}
return totalDuration;
}
function formatTimestamp(timestamp: number, settings: SimpleTimeTrackerSettings): string {
return moment.unix(timestamp).format(settings.timestampFormat);
}
function formatDurationBetween(startTime: number, endTime: number): string {
return formatDuration(moment.unix(endTime).diff(moment.unix(startTime)));
}
function formatDuration(totalTime: number): string {
let duration = moment.duration(totalTime);
2022-09-27 17:03:44 +02:00
let ret = "";
2022-09-27 21:23:36 +02:00
if (duration.hours() > 0)
2022-09-27 21:35:26 +02:00
ret += duration.hours() + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
2022-09-27 21:23:36 +02:00
return ret;
}
2022-09-27 16:06:40 +02:00
function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of tracker.entries)
table.push(createTableRow(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker))}**`]);
2022-09-27 16:06:40 +02:00
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)));
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";
let row: string[] = [];
for (let i = 0; i < 4; i++)
row.push(table[r][i].padEnd(widths[i], " "));
ret += row.join(" | ") + "\n";
2022-09-27 16:06:40 +02:00
}
return ret;
}
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of tracker.entries)
2022-09-28 14:11:49 +02:00
ret += createTableRow(entry, settings).join(settings.csvDelimiter) + "\n";
return ret;
}
function createTableRow(entry: Entry, settings: SimpleTimeTrackerSettings): string[] {
return [
entry.name,
formatTimestamp(entry.startTime, settings),
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
entry.endTime ? formatDurationBetween(entry.startTime, entry.endTime) : ""];
2022-09-27 16:06:40 +02:00
}