mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-12-19 03:39:23 +01:00
added the ability to create sub-entries
This commit is contained in:
parent
132e2088be
commit
1bda2accf4
3 changed files with 329 additions and 169 deletions
320
src/tracker.ts
320
src/tracker.ts
|
@ -9,23 +9,7 @@ export interface Entry {
|
||||||
name: string;
|
name: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
}
|
subEntries: Entry[];
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTracker(tracker: Tracker, app: App, section: MarkdownSectionInformation): Promise<void> {
|
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")
|
.setTooltip(running ? "End" : "Start")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (running) {
|
if (running) {
|
||||||
endEntry(tracker);
|
endRunningEntry(tracker);
|
||||||
} else {
|
} else {
|
||||||
startEntry(tracker, name.getValue());
|
startNewEntry(tracker, newSegmentNameBox.getValue());
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, this.app, getSectionInfo());
|
await saveTracker(tracker, this.app, getSectionInfo());
|
||||||
});
|
});
|
||||||
btn.buttonEl.addClass("simple-time-tracker-btn");
|
btn.buttonEl.addClass("simple-time-tracker-btn");
|
||||||
let name = new TextComponent(element)
|
let newSegmentNameBox = new TextComponent(element)
|
||||||
.setPlaceholder("Segment name")
|
.setPlaceholder("Segment name")
|
||||||
.setDisabled(running);
|
.setDisabled(running);
|
||||||
name.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" });
|
||||||
|
@ -95,19 +79,207 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
|
||||||
createEl("th", { text: "Duration" }),
|
createEl("th", { text: "Duration" }),
|
||||||
createEl("th"));
|
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 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 row = table.createEl("tr");
|
||||||
|
|
||||||
let name = row.createEl("td");
|
let name = row.createEl("td");
|
||||||
let namePar = name.createEl("span", { text: entry.name });
|
let namePar = name.createEl("span", { text: entry.name });
|
||||||
|
namePar.style.marginLeft = `${indent}em`;
|
||||||
let nameBox = new TextComponent(name).setValue(entry.name);
|
let nameBox = new TextComponent(name).setValue(entry.name);
|
||||||
nameBox.inputEl.hidden = true;
|
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 ? 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");
|
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)
|
let editButton = new ButtonComponent(entryButtons)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
.setTooltip("Edit")
|
.setTooltip("Edit")
|
||||||
|
@ -134,104 +306,12 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
|
||||||
.setTooltip("Remove")
|
.setTooltip("Remove")
|
||||||
.setIcon("lucide-trash")
|
.setIcon("lucide-trash")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
tracker.entries.remove(entry);
|
removeEntry(tracker.entries, entry);
|
||||||
await saveTracker(tracker, this.app, getSectionInfo());
|
await saveTracker(tracker, this.app, getSectionInfo());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (entry.subEntries) {
|
||||||
|
for (let sub of entry.subEntries)
|
||||||
|
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, getSectionInfo, settings, indent + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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
|
@ -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.
|
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
|
```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
|
Segment 5,22-09-28 01:28:15,22-09-28 01:28:18,3s
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue