added the ability to create sub-entries

This commit is contained in:
Ell 2022-10-19 16:35:21 +02:00
parent 132e2088be
commit 1bda2accf4
3 changed files with 329 additions and 169 deletions

View file

@ -9,23 +9,7 @@ export interface Entry {
name: string;
startTime: number;
endTime: number;
}
export function startEntry(tracker: Tracker, name: string): void {
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = { name: name, startTime: moment().unix(), endTime: null };
tracker.entries.push(entry);
};
export function endEntry(tracker: Tracker): void {
let last = tracker.entries.last();
last.endTime = moment().unix();
}
export function isRunning(tracker: Tracker): boolean {
let last = tracker.entries.last();
return last != null && !last.endTime;
subEntries: Entry[];
}
export async function saveTracker(tracker: Tracker, app: App, section: MarkdownSectionInformation): Promise<void> {
@ -64,17 +48,17 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
.setTooltip(running ? "End" : "Start")
.onClick(async () => {
if (running) {
endEntry(tracker);
endRunningEntry(tracker);
} else {
startEntry(tracker, name.getValue());
startNewEntry(tracker, newSegmentNameBox.getValue());
}
await saveTracker(tracker, this.app, getSectionInfo());
});
btn.buttonEl.addClass("simple-time-tracker-btn");
let name = new TextComponent(element)
let newSegmentNameBox = new TextComponent(element)
.setPlaceholder("Segment name")
.setDisabled(running);
name.inputEl.addClass("simple-time-tracker-txt");
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
// add timers
let timer = element.createDiv({ cls: "simple-time-tracker-timers" });
@ -95,19 +79,207 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
createEl("th", { text: "Duration" }),
createEl("th"));
for (let entry of tracker.entries)
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getSectionInfo, settings, 0);
// 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)
.setButtonText("Copy as CSV")
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
}
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);
}
function startSubEntry(entry: Entry, name: string) {
// 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;
}
if (!name)
name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({ name: name, startTime: moment().unix(), endTime: null, subEntries: null });
}
function startNewEntry(tracker: Tracker, name: string): void {
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = { name: name, startTime: moment().unix(), endTime: null, subEntries: null };
tracker.entries.push(entry);
};
function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().unix();
}
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
if (entries.contains(toRemove)) {
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 = null;
}
return true;
}
}
}
return false;
}
function isRunning(tracker: Tracker): boolean {
return !!getRunningEntry(tracker.entries);
}
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);
if (running)
return running;
} else {
// if this entry has no sub entries and no end time, it's running
if (!entry.endTime)
return entry;
}
}
return null;
}
function getDuration(entry: Entry) {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
let endTime = entry.endTime ? moment.unix(entry.endTime) : moment();
return endTime.diff(moment.unix(entry.startTime));
}
}
function getTotalDuration(entries: Entry[]): number {
let ret = 0;
for (let entry of entries)
ret += getDuration(entry);
return ret;
}
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement) {
let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running)));
currentDiv.hidden = false;
} else {
currentDiv.hidden = true;
}
total.setText(formatDuration(getTotalDuration(tracker.entries)));
}
function formatTimestamp(timestamp: number, settings: SimpleTimeTrackerSettings): string {
return moment.unix(timestamp).format(settings.timestampFormat);
}
function formatDuration(totalTime: number): string {
let duration = moment.duration(totalTime);
let ret = "";
if (duration.hours() > 0)
ret += duration.hours() + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
return ret;
}
function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of tracker.entries)
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries))}**`]);
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";
}
return ret;
}
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of tracker.entries) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
return ret;
}
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
let ret: string[][] = [[
entry.name,
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry)) : ""]];
if (entry.subEntries) {
for (let sub of entry.subEntries)
ret.push(...createTableSection(sub, settings));
}
return ret;
}
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, running: boolean, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number) {
let row = table.createEl("tr");
let name = row.createEl("td");
let namePar = name.createEl("span", { text: entry.name });
namePar.style.marginLeft = `${indent}em`;
let nameBox = new TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", { text: formatTimestamp(entry.startTime, settings) });
row.createEl("td", { text: entry.startTime ? formatTimestamp(entry.startTime, settings) : "" });
row.createEl("td", { text: entry.endTime ? formatTimestamp(entry.endTime, settings) : "" });
row.createEl("td", { text: entry.endTime ? formatDurationBetween(entry.startTime, entry.endTime) : "" });
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry)) : "" });
let entryButtons = row.createEl("td");
if (!running) {
new ButtonComponent(entryButtons)
.setClass("clickable-icon")
.setIcon(`lucide-play`)
.setTooltip("Continue")
.onClick(async () => {
startSubEntry(entry, newSegmentNameBox.getValue());
await saveTracker(tracker, this.app, getSectionInfo());
});
}
let editButton = new ButtonComponent(entryButtons)
.setClass("clickable-icon")
.setTooltip("Edit")
@ -134,104 +306,12 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
.setTooltip("Remove")
.setIcon("lucide-trash")
.onClick(async () => {
tracker.entries.remove(entry);
removeEntry(tracker.entries, entry);
await saveTracker(tracker, this.app, getSectionInfo());
});
}
// 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)
.setButtonText("Copy as CSV")
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
if (entry.subEntries) {
for (let sub of entry.subEntries)
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, getSectionInfo, settings, indent + 1);
}
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);
}
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);
let ret = "";
if (duration.hours() > 0)
ret += duration.hours() + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
return ret;
}
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))}**`]);
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";
}
return ret;
}
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of tracker.entries)
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) : ""];
}

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
These are the notes for my cool project. There's so much left to do! I wish I had a way to track the amount of time I spend on each part of the project.
```simple-time-tracker
{"entries":[{"name":"Think about project","startTime":1664305777,"endTime":1664308788},{"name":"Create project note","startTime":1664308810,"endTime":1664308815},{"name":"Work on project","startTime":1664308830,"endTime":1664309301},{"name":"Segment 4","startTime":1664364444,"endTime":1664364449},{"name":"Segment 5","startTime":1664364495,"endTime":1664364498},{"name":"Segment 6","startTime":1664458520,"endTime":1664458523},{"name":"Segment 7","startTime":1664460326,"endTime":1664460329}]}
{"entries":[{"name":"Segment 1","startTime":1666189948,"endTime":1666189951,"subEntries":null},{"name":"Segment 2","startTime":1666189953,"endTime":1666189961,"subEntries":null},{"name":"Segment 3","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":1666189962,"endTime":1666189995,"subEntries":null},{"name":"Part 2","startTime":1666190004,"endTime":1666190025,"subEntries":null},{"name":"Part 3","startTime":1666190094,"endTime":1666190101,"subEntries":null}]},{"name":"Segment 4","startTime":1666190088,"endTime":1666190091,"subEntries":null}]}
```
```
@ -13,6 +13,3 @@ Segment 4,22-09-28 01:27:24,22-09-28 01:27:29,5s
Segment 5,22-09-28 01:28:15,22-09-28 01:28:18,3s
```