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) {
|
2022-09-28 14:09:21 +02:00
|
|
|
// 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" }),
|
2022-10-10 14:40:36 +02:00
|
|
|
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");
|
2022-10-10 14:40:36 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2022-09-28 14:09:21 +02:00
|
|
|
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) : "" });
|
2022-10-10 14:40:36 +02:00
|
|
|
|
|
|
|
let entryButtons = row.createEl("td");
|
|
|
|
let editButton = new ButtonComponent(entryButtons)
|
|
|
|
.setClass("clickable-icon")
|
|
|
|
.setTooltip("Edit")
|
|
|
|
.setIcon("lucide-pencil")
|
|
|
|
.onClick(() => {
|
|
|
|
if (namePar.hidden) {
|
|
|
|
namePar.hidden = false;
|
|
|
|
nameBox.inputEl.hidden = true;
|
|
|
|
if (nameBox.getValue())
|
|
|
|
namePar.setText(nameBox.getValue());
|
|
|
|
editButton.setIcon("lucide-pencil");
|
|
|
|
} else {
|
|
|
|
namePar.hidden = true;
|
|
|
|
nameBox.inputEl.hidden = false;
|
|
|
|
nameBox.setValue(namePar.getText());
|
|
|
|
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
|
|
|
}
|
2022-09-28 14:09:21 +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")
|
2022-09-28 14:09:21 +02:00
|
|
|
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
|
2022-09-27 21:23:36 +02:00
|
|
|
}
|
|
|
|
|
2022-09-28 14:09:21 +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
|
|
|
|
2022-09-28 14:09:21 +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
|
|
|
|
2022-09-28 14:09:21 +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
|
|
|
|
2022-09-28 14:09:21 +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
|
|
|
}
|
2022-09-28 14:09:21 +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";
|
2022-09-28 14:09:21 +02:00
|
|
|
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
|
|
|
}
|