mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-11-15 23:13:12 +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
318
src/tracker.ts
318
src/tracker.ts
|
@ -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
|
@ -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
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue