mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-11-24 01:58:35 +01:00
Compare commits
2 commits
ff48e05403
...
cc0d336d96
Author | SHA1 | Date | |
---|---|---|---|
|
cc0d336d96 | ||
|
d4188127b3 |
1 changed files with 241 additions and 210 deletions
451
src/tracker.ts
451
src/tracker.ts
|
@ -1,327 +1,358 @@
|
||||||
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian";
|
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian"
|
||||||
import { SimpleTimeTrackerSettings } from "./settings";
|
import { SimpleTimeTrackerSettings } from "./settings"
|
||||||
import { ConfirmModal } from "./confirm-modal";
|
import { ConfirmModal } from "./confirm-modal"
|
||||||
|
|
||||||
export interface Tracker {
|
export interface Tracker {
|
||||||
entries: Entry[];
|
entries: Entry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
name: string;
|
name: string
|
||||||
startTime: string;
|
startTime: string
|
||||||
endTime: string;
|
endTime: string
|
||||||
subEntries?: Entry[];
|
subEntries?: Entry[]
|
||||||
collapsed?: boolean;
|
collapsed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> {
|
export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> {
|
||||||
let file = app.vault.getAbstractFileByPath(fileName) as TFile;
|
let file = app.vault.getAbstractFileByPath(fileName) as TFile
|
||||||
if (!file)
|
if (!file)
|
||||||
return;
|
return
|
||||||
let content = await app.vault.read(file);
|
let content = await app.vault.read(file)
|
||||||
|
|
||||||
// figure out what part of the content we have to edit
|
// figure out what part of the content we have to edit
|
||||||
let lines = content.split("\n");
|
let lines = content.split("\n")
|
||||||
let prev = lines.filter((_, i) => i <= section.lineStart).join("\n");
|
let prev = lines.filter((_, i) => i <= section.lineStart).join("\n")
|
||||||
let next = lines.filter((_, i) => i >= section.lineEnd).join("\n");
|
let next = lines.filter((_, i) => i >= section.lineEnd).join("\n")
|
||||||
// edit only the code block content, leave the rest untouched
|
// edit only the code block content, leave the rest untouched
|
||||||
content = `${prev}\n${JSON.stringify(tracker)}\n${next}`;
|
content = `${prev}\n${JSON.stringify(tracker)}\n${next}`
|
||||||
|
|
||||||
await app.vault.modify(file, content);
|
await app.vault.modify(file, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadTracker(json: string): Tracker {
|
export function loadTracker(json: string): Tracker {
|
||||||
if (json) {
|
if (json) {
|
||||||
try {
|
try {
|
||||||
let ret = JSON.parse(json);
|
let ret = JSON.parse(json)
|
||||||
updateLegacyInfo(ret.entries);
|
updateLegacyInfo(ret.entries)
|
||||||
return ret;
|
return ret
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Failed to parse Tracker from ${json}`);
|
console.log(`Failed to parse Tracker from ${json}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { entries: [] };
|
return { entries: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAllTrackers(fileName: string): Promise<{ section: MarkdownSectionInformation, tracker: Tracker }[]> {
|
export async function loadAllTrackers(fileName: string): Promise<{ section: MarkdownSectionInformation, tracker: Tracker }[]> {
|
||||||
let file = app.vault.getAbstractFileByPath(fileName);
|
let file = app.vault.getAbstractFileByPath(fileName)
|
||||||
let content = (await app.vault.cachedRead(file as TFile)).split("\n");
|
let content = (await app.vault.cachedRead(file as TFile)).split("\n")
|
||||||
|
|
||||||
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = [];
|
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = []
|
||||||
let curr: Partial<MarkdownSectionInformation> | undefined;
|
let curr: Partial<MarkdownSectionInformation> | undefined
|
||||||
for (let i = 0; i < content.length; i++) {
|
for (let i = 0; i < content.length; i++) {
|
||||||
let line = content[i];
|
let line = content[i]
|
||||||
if (line.trimEnd() == "```simple-time-tracker") {
|
if (line.trimEnd() == "```simple-time-tracker") {
|
||||||
curr = { lineStart: i + 1, text: "" };
|
curr = { lineStart: i + 1, text: "" }
|
||||||
} else if (curr) {
|
} else if (curr) {
|
||||||
if (line.trimEnd() == "```") {
|
if (line.trimEnd() == "```") {
|
||||||
curr.lineEnd = i - 1;
|
curr.lineEnd = i - 1
|
||||||
let tracker = loadTracker(curr.text);
|
let tracker = loadTracker(curr.text)
|
||||||
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker });
|
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker })
|
||||||
curr = undefined;
|
curr = undefined
|
||||||
} else {
|
} else {
|
||||||
curr.text += `${line}\n`;
|
curr.text += `${line}\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return trackers;
|
return trackers
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetFile = () => string;
|
type GetFile = () => string
|
||||||
|
|
||||||
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
|
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
|
||||||
|
|
||||||
element.addClass("simple-time-tracker-container");
|
element.addClass("simple-time-tracker-container")
|
||||||
// add start/stop controls
|
// add start/stop controls
|
||||||
let running = isRunning(tracker);
|
let running = isRunning(tracker)
|
||||||
let btn = new ButtonComponent(element)
|
let btn = new ButtonComponent(element)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
.setIcon(`lucide-${running ? "stop" : "play"}-circle`)
|
.setIcon(`lucide-${running ? "stop" : "play"}-circle`)
|
||||||
.setTooltip(running ? "End" : "Start")
|
.setTooltip(running ? "End" : "Start")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (running) {
|
if (running) {
|
||||||
endRunningEntry(tracker);
|
endRunningEntry(tracker)
|
||||||
} else {
|
} else {
|
||||||
startNewEntry(tracker, newSegmentNameBox.getValue());
|
startNewEntry(tracker, newSegmentNameBox.getValue())
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||||
});
|
})
|
||||||
btn.buttonEl.addClass("simple-time-tracker-btn");
|
btn.buttonEl.addClass("simple-time-tracker-btn")
|
||||||
let newSegmentNameBox = new TextComponent(element)
|
let newSegmentNameBox = new TextComponent(element)
|
||||||
.setPlaceholder("Segment name")
|
.setPlaceholder("Segment name")
|
||||||
.setDisabled(running);
|
.setDisabled(running)
|
||||||
newSegmentNameBox.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" })
|
||||||
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
|
||||||
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" });
|
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" })
|
||||||
currentDiv.createEl("span", { text: "Current" });
|
currentDiv.createEl("span", { text: "Current" })
|
||||||
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
|
||||||
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" });
|
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" })
|
||||||
totalDiv.createEl("span", { text: "Total" });
|
totalDiv.createEl("span", { text: "Total" })
|
||||||
|
let totalTodayDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" })
|
||||||
|
let totalToday = totalTodayDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" })
|
||||||
|
totalTodayDiv.createEl("span", { text: "Total Today" })
|
||||||
|
|
||||||
if (tracker.entries.length > 0) {
|
if (tracker.entries.length > 0) {
|
||||||
// add table
|
// add table
|
||||||
let table = element.createEl("table", { cls: "simple-time-tracker-table" });
|
let table = element.createEl("table", { cls: "simple-time-tracker-table" })
|
||||||
table.createEl("tr").append(
|
table.createEl("tr").append(
|
||||||
createEl("th", { text: "Segment" }),
|
createEl("th", { text: "Segment" }),
|
||||||
createEl("th", { text: "Start time" }),
|
createEl("th", { text: "Start time" }),
|
||||||
createEl("th", { text: "End time" }),
|
createEl("th", { text: "End time" }),
|
||||||
createEl("th", { text: "Duration" }),
|
createEl("th", { text: "Duration" }),
|
||||||
createEl("th"));
|
createEl("th"))
|
||||||
|
|
||||||
for (let entry of orderedEntries(tracker.entries, settings))
|
for (let entry of orderedEntries(tracker.entries, settings))
|
||||||
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
|
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component)
|
||||||
|
|
||||||
// add copy buttons
|
// add copy buttons
|
||||||
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
|
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" })
|
||||||
new ButtonComponent(buttons)
|
new ButtonComponent(buttons)
|
||||||
.setButtonText("Copy as table")
|
.setButtonText("Copy as table")
|
||||||
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
|
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)))
|
||||||
new ButtonComponent(buttons)
|
new ButtonComponent(buttons)
|
||||||
.setButtonText("Copy as CSV")
|
.setButtonText("Copy as CSV")
|
||||||
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)));
|
.onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setCountdownValues(tracker, current, total, currentDiv, settings);
|
setCountdownValues(tracker, current, total, totalToday, currentDiv, settings)
|
||||||
let intervalId = window.setInterval(() => {
|
let intervalId = window.setInterval(() => {
|
||||||
// we delete the interval timer when the element is removed
|
// we delete the interval timer when the element is removed
|
||||||
if (!element.isConnected) {
|
if (!element.isConnected) {
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setCountdownValues(tracker, current, total, currentDiv, settings);
|
setCountdownValues(tracker, current, total, totalToday, currentDiv, settings)
|
||||||
}, 1000);
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDuration(entry: Entry): number {
|
export function getDuration(entry: Entry): number {
|
||||||
if (entry.subEntries) {
|
if (entry.subEntries) {
|
||||||
return getTotalDuration(entry.subEntries);
|
return getTotalDuration(entry.subEntries)
|
||||||
} else {
|
} else {
|
||||||
let endTime = entry.endTime ? moment(entry.endTime) : moment();
|
let endTime = entry.endTime ? moment(entry.endTime) : moment()
|
||||||
return endTime.diff(moment(entry.startTime));
|
return endTime.diff(moment(entry.startTime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDurationToday(entry: Entry): number {
|
||||||
|
if (entry.subEntries) {
|
||||||
|
return getTotalDurationToday(entry.subEntries)
|
||||||
|
} else {
|
||||||
|
let today = moment().startOf('day')
|
||||||
|
let endTime = entry.endTime ? moment(entry.endTime) : moment()
|
||||||
|
let startTime = moment(entry.startTime)
|
||||||
|
|
||||||
|
if (endTime.isBefore(today)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTime.isBefore(today)) {
|
||||||
|
startTime = today
|
||||||
|
}
|
||||||
|
|
||||||
|
return endTime.diff(startTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotalDuration(entries: Entry[]): number {
|
export function getTotalDuration(entries: Entry[]): number {
|
||||||
let ret = 0;
|
let ret = 0
|
||||||
for (let entry of entries)
|
for (let entry of entries)
|
||||||
ret += getDuration(entry);
|
ret += getDuration(entry)
|
||||||
return ret;
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalDurationToday(entries: Entry[]): number {
|
||||||
|
let ret = 0
|
||||||
|
for (let entry of entries)
|
||||||
|
ret += getDurationToday(entry)
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRunning(tracker: Tracker): boolean {
|
export function isRunning(tracker: Tracker): boolean {
|
||||||
return !!getRunningEntry(tracker.entries);
|
return !!getRunningEntry(tracker.entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRunningEntry(entries: Entry[]): Entry {
|
export function getRunningEntry(entries: Entry[]): Entry {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
// if this entry has sub entries, check if one of them is running
|
// if this entry has sub entries, check if one of them is running
|
||||||
if (entry.subEntries) {
|
if (entry.subEntries) {
|
||||||
let running = getRunningEntry(entry.subEntries);
|
let running = getRunningEntry(entry.subEntries)
|
||||||
if (running)
|
if (running)
|
||||||
return running;
|
return running
|
||||||
} else {
|
} else {
|
||||||
// if this entry has no sub entries and no end time, it's running
|
// if this entry has no sub entries and no end time, it's running
|
||||||
if (!entry.endTime)
|
if (!entry.endTime)
|
||||||
return entry;
|
return entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
||||||
let table = [["Segment", "Start time", "End time", "Duration"]];
|
let table = [["Segment", "Start time", "End time", "Duration"]]
|
||||||
for (let entry of orderedEntries(tracker.entries, settings))
|
for (let entry of orderedEntries(tracker.entries, settings))
|
||||||
table.push(...createTableSection(entry, settings));
|
table.push(...createTableSection(entry, settings))
|
||||||
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
|
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`])
|
||||||
|
|
||||||
let ret = "";
|
let ret = ""
|
||||||
// calculate the width every column needs to look neat when monospaced
|
// 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)));
|
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++) {
|
for (let r = 0; r < table.length; r++) {
|
||||||
// add separators after first row
|
// add separators after first row
|
||||||
if (r == 1)
|
if (r == 1)
|
||||||
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n";
|
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n"
|
||||||
|
|
||||||
let row: string[] = [];
|
let row: string[] = []
|
||||||
for (let i = 0; i < 4; i++)
|
for (let i = 0; i < 4; i++)
|
||||||
row.push(table[r][i].padEnd(widths[i], " "));
|
row.push(table[r][i].padEnd(widths[i], " "))
|
||||||
ret += "| " + row.join(" | ") + " |\n";
|
ret += "| " + row.join(" | ") + " |\n"
|
||||||
}
|
}
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
||||||
let ret = "";
|
let ret = ""
|
||||||
for (let entry of orderedEntries(tracker.entries, settings)) {
|
for (let entry of orderedEntries(tracker.entries, settings)) {
|
||||||
for (let row of createTableSection(entry, settings))
|
for (let row of createTableSection(entry, settings))
|
||||||
ret += row.join(settings.csvDelimiter) + "\n";
|
ret += row.join(settings.csvDelimiter) + "\n"
|
||||||
}
|
}
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
|
export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
|
||||||
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
|
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
return moment(timestamp).format(settings.timestampFormat);
|
return moment(timestamp).format(settings.timestampFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
|
export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
|
||||||
let ret = "";
|
let ret = ""
|
||||||
let duration = moment.duration(totalTime);
|
let duration = moment.duration(totalTime)
|
||||||
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
|
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours())
|
||||||
|
|
||||||
if (settings.timestampDurations) {
|
if (settings.timestampDurations) {
|
||||||
if (settings.fineGrainedDurations) {
|
if (settings.fineGrainedDurations) {
|
||||||
let days = Math.floor(duration.asDays());
|
let days = Math.floor(duration.asDays())
|
||||||
if (days > 0)
|
if (days > 0)
|
||||||
ret += days + ".";
|
ret += days + "."
|
||||||
}
|
}
|
||||||
ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`;
|
ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`
|
||||||
} else {
|
} else {
|
||||||
if (settings.fineGrainedDurations) {
|
if (settings.fineGrainedDurations) {
|
||||||
let years = Math.floor(duration.asYears());
|
let years = Math.floor(duration.asYears())
|
||||||
if (years > 0)
|
if (years > 0)
|
||||||
ret += years + "y ";
|
ret += years + "y "
|
||||||
if (duration.months() > 0)
|
if (duration.months() > 0)
|
||||||
ret += duration.months() + "M ";
|
ret += duration.months() + "M "
|
||||||
if (duration.days() > 0)
|
if (duration.days() > 0)
|
||||||
ret += duration.days() + "d ";
|
ret += duration.days() + "d "
|
||||||
}
|
}
|
||||||
if (hours > 0)
|
if (hours > 0)
|
||||||
ret += hours + "h ";
|
ret += hours + "h "
|
||||||
if (duration.minutes() > 0)
|
if (duration.minutes() > 0)
|
||||||
ret += duration.minutes() + "m ";
|
ret += duration.minutes() + "m "
|
||||||
ret += duration.seconds() + "s";
|
ret += duration.seconds() + "s"
|
||||||
}
|
}
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function startSubEntry(entry: Entry, name: string): void {
|
function startSubEntry(entry: Entry, name: string): void {
|
||||||
// if this entry is not split yet, we add its time as a sub-entry instead
|
// if this entry is not split yet, we add its time as a sub-entry instead
|
||||||
if (!entry.subEntries) {
|
if (!entry.subEntries) {
|
||||||
entry.subEntries = [{ ...entry, name: `Part 1` }];
|
entry.subEntries = [{ ...entry, name: `Part 1` }]
|
||||||
entry.startTime = null;
|
entry.startTime = null
|
||||||
entry.endTime = null;
|
entry.endTime = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name)
|
if (!name)
|
||||||
name = `Part ${entry.subEntries.length + 1}`;
|
name = `Part ${entry.subEntries.length + 1}`
|
||||||
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined });
|
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined })
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNewEntry(tracker: Tracker, name: string): void {
|
function startNewEntry(tracker: Tracker, name: string): void {
|
||||||
if (!name)
|
if (!name)
|
||||||
name = `Segment ${tracker.entries.length + 1}`;
|
name = `Segment ${tracker.entries.length + 1}`
|
||||||
let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined };
|
let entry: Entry = { name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined }
|
||||||
tracker.entries.push(entry);
|
tracker.entries.push(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
function endRunningEntry(tracker: Tracker): void {
|
function endRunningEntry(tracker: Tracker): void {
|
||||||
let entry = getRunningEntry(tracker.entries);
|
let entry = getRunningEntry(tracker.entries)
|
||||||
entry.endTime = moment().toISOString();
|
entry.endTime = moment().toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
|
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
|
||||||
if (entries.contains(toRemove)) {
|
if (entries.contains(toRemove)) {
|
||||||
entries.remove(toRemove);
|
entries.remove(toRemove)
|
||||||
return true;
|
return true
|
||||||
} else {
|
} else {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
|
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
|
||||||
// if we only have one sub entry remaining, we can merge back into our main entry
|
// if we only have one sub entry remaining, we can merge back into our main entry
|
||||||
if (entry.subEntries.length == 1) {
|
if (entry.subEntries.length == 1) {
|
||||||
let single = entry.subEntries[0];
|
let single = entry.subEntries[0]
|
||||||
entry.startTime = single.startTime;
|
entry.startTime = single.startTime
|
||||||
entry.endTime = single.endTime;
|
entry.endTime = single.endTime
|
||||||
entry.subEntries = undefined;
|
entry.subEntries = undefined
|
||||||
}
|
}
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, totalToday: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
||||||
let running = getRunningEntry(tracker.entries);
|
let running = getRunningEntry(tracker.entries)
|
||||||
if (running && !running.endTime) {
|
if (running && !running.endTime) {
|
||||||
current.setText(formatDuration(getDuration(running), settings));
|
current.setText(formatDuration(getDuration(running), settings))
|
||||||
currentDiv.hidden = false;
|
currentDiv.hidden = false
|
||||||
} else {
|
} else {
|
||||||
currentDiv.hidden = true;
|
currentDiv.hidden = true
|
||||||
}
|
}
|
||||||
total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
|
total.setText(formatDuration(getTotalDuration(tracker.entries), settings))
|
||||||
|
totalToday.setText(formatDuration(getTotalDurationToday(tracker.entries), settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
return moment(timestamp).format(settings.editableTimestampFormat);
|
return moment(timestamp).format(settings.editableTimestampFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
|
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
return moment(formatted, settings.editableTimestampFormat).toISOString();
|
return moment(formatted, settings.editableTimestampFormat).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLegacyInfo(entries: Entry[]): void {
|
function updateLegacyInfo(entries: Entry[]): void {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
// in 0.1.8, timestamps were changed from unix to iso
|
// in 0.1.8, timestamps were changed from unix to iso
|
||||||
if (entry.startTime && !isNaN(+entry.startTime))
|
if (entry.startTime && !isNaN(+entry.startTime))
|
||||||
entry.startTime = moment.unix(+entry.startTime).toISOString();
|
entry.startTime = moment.unix(+entry.startTime).toISOString()
|
||||||
if (entry.endTime && !isNaN(+entry.endTime))
|
if (entry.endTime && !isNaN(+entry.endTime))
|
||||||
entry.endTime = moment.unix(+entry.endTime).toISOString();
|
entry.endTime = moment.unix(+entry.endTime).toISOString()
|
||||||
|
|
||||||
// in 1.0.0, sub-entries were made optional
|
// in 1.0.0, sub-entries were made optional
|
||||||
if (entry.subEntries == null || !entry.subEntries.length)
|
if (entry.subEntries == null || !entry.subEntries.length)
|
||||||
entry.subEntries = undefined;
|
entry.subEntries = undefined
|
||||||
|
|
||||||
if (entry.subEntries)
|
if (entry.subEntries)
|
||||||
updateLegacyInfo(entry.subEntries);
|
updateLegacyInfo(entry.subEntries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,25 +362,25 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings):
|
||||||
entry.name,
|
entry.name,
|
||||||
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
|
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
|
||||||
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
|
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
|
||||||
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]];
|
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]]
|
||||||
if (entry.subEntries) {
|
if (entry.subEntries) {
|
||||||
for (let sub of orderedEntries(entry.subEntries, settings))
|
for (let sub of orderedEntries(entry.subEntries, settings))
|
||||||
ret.push(...createTableSection(sub, settings));
|
ret.push(...createTableSection(sub, settings))
|
||||||
}
|
}
|
||||||
return ret;
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
|
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
|
||||||
let entryRunning = getRunningEntry(tracker.entries) == entry;
|
let entryRunning = getRunningEntry(tracker.entries) == entry
|
||||||
let row = table.createEl("tr");
|
let row = table.createEl("tr")
|
||||||
|
|
||||||
let nameField = new EditableField(row, indent, entry.name);
|
let nameField = new EditableField(row, indent, entry.name)
|
||||||
let startField = new EditableTimestampField(row, (entry.startTime), settings);
|
let startField = new EditableTimestampField(row, (entry.startTime), settings)
|
||||||
let endField = new EditableTimestampField(row, (entry.endTime), settings);
|
let endField = new EditableTimestampField(row, (entry.endTime), settings)
|
||||||
|
|
||||||
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" });
|
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" })
|
||||||
|
|
||||||
renderNameAsMarkdown(nameField.label, getFile, component);
|
renderNameAsMarkdown(nameField.label, getFile, component)
|
||||||
|
|
||||||
let expandButton = new ButtonComponent(nameField.label)
|
let expandButton = new ButtonComponent(nameField.label)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
|
@ -357,56 +388,56 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
.setIcon(`chevron-${entry.collapsed ? "left" : "down"}`)
|
.setIcon(`chevron-${entry.collapsed ? "left" : "down"}`)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (entry.collapsed) {
|
if (entry.collapsed) {
|
||||||
entry.collapsed = undefined;
|
entry.collapsed = undefined
|
||||||
} else {
|
} else {
|
||||||
entry.collapsed = true;
|
entry.collapsed = true
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||||
});
|
})
|
||||||
if (!entry.subEntries)
|
if (!entry.subEntries)
|
||||||
expandButton.buttonEl.style.visibility = "hidden";
|
expandButton.buttonEl.style.visibility = "hidden"
|
||||||
|
|
||||||
let entryButtons = row.createEl("td");
|
let entryButtons = row.createEl("td")
|
||||||
entryButtons.addClass("simple-time-tracker-table-buttons");
|
entryButtons.addClass("simple-time-tracker-table-buttons")
|
||||||
new ButtonComponent(entryButtons)
|
new ButtonComponent(entryButtons)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
.setIcon(`lucide-play`)
|
.setIcon(`lucide-play`)
|
||||||
.setTooltip("Continue")
|
.setTooltip("Continue")
|
||||||
.setDisabled(trackerRunning)
|
.setDisabled(trackerRunning)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
startSubEntry(entry, newSegmentNameBox.getValue());
|
startSubEntry(entry, newSegmentNameBox.getValue())
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||||
});
|
})
|
||||||
let editButton = new ButtonComponent(entryButtons)
|
let editButton = new ButtonComponent(entryButtons)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
.setTooltip("Edit")
|
.setTooltip("Edit")
|
||||||
.setIcon("lucide-pencil")
|
.setIcon("lucide-pencil")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (nameField.editing()) {
|
if (nameField.editing()) {
|
||||||
entry.name = nameField.endEdit();
|
entry.name = nameField.endEdit()
|
||||||
expandButton.buttonEl.style.display = null;
|
expandButton.buttonEl.style.display = null
|
||||||
startField.endEdit();
|
startField.endEdit()
|
||||||
entry.startTime = startField.getTimestamp();
|
entry.startTime = startField.getTimestamp()
|
||||||
if (!entryRunning) {
|
if (!entryRunning) {
|
||||||
endField.endEdit();
|
endField.endEdit()
|
||||||
entry.endTime = endField.getTimestamp();
|
entry.endTime = endField.getTimestamp()
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||||
editButton.setIcon("lucide-pencil");
|
editButton.setIcon("lucide-pencil")
|
||||||
|
|
||||||
renderNameAsMarkdown(nameField.label, getFile, component);
|
renderNameAsMarkdown(nameField.label, getFile, component)
|
||||||
} else {
|
} else {
|
||||||
nameField.beginEdit(entry.name);
|
nameField.beginEdit(entry.name)
|
||||||
expandButton.buttonEl.style.display = "none";
|
expandButton.buttonEl.style.display = "none"
|
||||||
// only allow editing start and end times if we don't have sub entries
|
// only allow editing start and end times if we don't have sub entries
|
||||||
if (!entry.subEntries) {
|
if (!entry.subEntries) {
|
||||||
startField.beginEdit(entry.startTime);
|
startField.beginEdit(entry.startTime)
|
||||||
if (!entryRunning)
|
if (!entryRunning)
|
||||||
endField.beginEdit(entry.endTime);
|
endField.beginEdit(entry.endTime)
|
||||||
}
|
}
|
||||||
editButton.setIcon("lucide-check");
|
editButton.setIcon("lucide-check")
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
new ButtonComponent(entryButtons)
|
new ButtonComponent(entryButtons)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
.setTooltip("Remove")
|
.setTooltip("Remove")
|
||||||
|
@ -414,100 +445,100 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
.setDisabled(entryRunning)
|
.setDisabled(entryRunning)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
|
|
||||||
const confirmed = await showConfirm("Are you sure you want to delete this entry?");
|
const confirmed = await showConfirm("Are you sure you want to delete this entry?")
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEntry(tracker.entries, entry);
|
removeEntry(tracker.entries, entry)
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, getFile(), getSectionInfo())
|
||||||
});
|
})
|
||||||
|
|
||||||
if (entry.subEntries && !entry.collapsed) {
|
if (entry.subEntries && !entry.collapsed) {
|
||||||
for (let sub of orderedEntries(entry.subEntries, settings))
|
for (let sub of orderedEntries(entry.subEntries, settings))
|
||||||
addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
|
addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirm(message: string): Promise<boolean> {
|
function showConfirm(message: string): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const modal = new ConfirmModal(app, message, resolve);
|
const modal = new ConfirmModal(app, message, resolve)
|
||||||
modal.open();
|
modal.open()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
|
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
|
||||||
// we don't have to wait here since async code only occurs when a file needs to be loaded (like a linked image)
|
// we don't have to wait here since async code only occurs when a file needs to be loaded (like a linked image)
|
||||||
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
|
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component)
|
||||||
// rendering wraps it in a paragraph
|
// rendering wraps it in a paragraph
|
||||||
label.innerHTML = label.querySelector("p").innerHTML;
|
label.innerHTML = label.querySelector("p").innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EditableField {
|
class EditableField {
|
||||||
cell: HTMLTableCellElement;
|
cell: HTMLTableCellElement
|
||||||
label: HTMLSpanElement;
|
label: HTMLSpanElement
|
||||||
box: TextComponent;
|
box: TextComponent
|
||||||
|
|
||||||
constructor(row: HTMLTableRowElement, indent: number, value: string) {
|
constructor(row: HTMLTableRowElement, indent: number, value: string) {
|
||||||
this.cell = row.createEl("td");
|
this.cell = row.createEl("td")
|
||||||
this.label = this.cell.createEl("span", { text: value });
|
this.label = this.cell.createEl("span", { text: value })
|
||||||
this.label.style.marginLeft = `${indent}em`;
|
this.label.style.marginLeft = `${indent}em`
|
||||||
this.box = new TextComponent(this.cell).setValue(value);
|
this.box = new TextComponent(this.cell).setValue(value)
|
||||||
this.box.inputEl.addClass("simple-time-tracker-input");
|
this.box.inputEl.addClass("simple-time-tracker-input")
|
||||||
this.box.inputEl.hide();
|
this.box.inputEl.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
editing(): boolean {
|
editing(): boolean {
|
||||||
return this.label.hidden;
|
return this.label.hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
beginEdit(value: string): void {
|
beginEdit(value: string): void {
|
||||||
this.label.hidden = true;
|
this.label.hidden = true
|
||||||
this.box.setValue(value);
|
this.box.setValue(value)
|
||||||
this.box.inputEl.show();
|
this.box.inputEl.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
endEdit(): string {
|
endEdit(): string {
|
||||||
const value = this.box.getValue();
|
const value = this.box.getValue()
|
||||||
this.label.setText(value);
|
this.label.setText(value)
|
||||||
this.box.inputEl.hide();
|
this.box.inputEl.hide()
|
||||||
this.label.hidden = false;
|
this.label.hidden = false
|
||||||
return value;
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EditableTimestampField extends EditableField {
|
class EditableTimestampField extends EditableField {
|
||||||
settings: SimpleTimeTrackerSettings;
|
settings: SimpleTimeTrackerSettings
|
||||||
|
|
||||||
constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) {
|
constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) {
|
||||||
super(row, 0, value ? formatTimestamp(value, settings) : "");
|
super(row, 0, value ? formatTimestamp(value, settings) : "")
|
||||||
this.settings = settings;
|
this.settings = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
beginEdit(value: string): void {
|
beginEdit(value: string): void {
|
||||||
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "");
|
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
endEdit(): string {
|
endEdit(): string {
|
||||||
const value = this.box.getValue();
|
const value = this.box.getValue()
|
||||||
let displayValue = value;
|
let displayValue = value
|
||||||
if (value) {
|
if (value) {
|
||||||
const timestamp = unformatEditableTimestamp(value, this.settings);
|
const timestamp = unformatEditableTimestamp(value, this.settings)
|
||||||
displayValue = formatTimestamp(timestamp, this.settings);
|
displayValue = formatTimestamp(timestamp, this.settings)
|
||||||
}
|
}
|
||||||
this.label.setText(displayValue);
|
this.label.setText(displayValue)
|
||||||
this.box.inputEl.hide();
|
this.box.inputEl.hide()
|
||||||
this.label.hidden = false;
|
this.label.hidden = false
|
||||||
return value;
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
getTimestamp(): string {
|
getTimestamp(): string {
|
||||||
if (this.box.getValue()) {
|
if (this.box.getValue()) {
|
||||||
return unformatEditableTimestamp(this.box.getValue(), this.settings);
|
return unformatEditableTimestamp(this.box.getValue(), this.settings)
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue