diff --git a/README.md b/README.md index abb0240..be1294d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Tag-Based Time Tracker -Multi-purpose time tracker for your notes! ![A screenshot of the plugin in action](reporting-screenshot.png) @@ -7,7 +6,7 @@ This is a fork of [super-simple-plugin](https://github.com/Ellpeck/ObsidianSimpl # TLDR -1. Execute the `Super Simple Time Tracker: Insert Time Tracker` command to start logging. +1. Execute the `Tag Based Time Tracker: Insert Time Tracker` command to start logging. 2. Set your tags in the settings in YAML format: ```yaml @@ -80,17 +79,17 @@ clients: ``` ## Tracker Data in Dataview -Super Simple Time Tracker has a public API that can be used with [Dataview](https://blacksmithgu.github.io/obsidian-dataview/), specifically [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/api/intro/), which can be accessed using the following code: +Tag Based Time Tracker has a public API that can be used with [Dataview](https://blacksmithgu.github.io/obsidian-dataview/), specifically [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/api/intro/), which can be accessed using the following code: ```js -dv.app.plugins.plugins["simple-time-tracker"].api; +dv.app.plugins.plugins["tag-based-time-tracker"].api; ``` The following is a short example that uses DataviewJS to load all trackers in the vault and print the total duration of each tracker: ```js // Get the time tracker plugin API instance -let api = dv.app.plugins.plugins["simple-time-tracker"].api; +let api = dv.app.plugins.plugins["tag-based-time-tracker"].api; for (let page of dv.pages()) { // Load trackers in the file with the given path diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 88122ef..2bfe4a2 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -46,7 +46,7 @@ esbuild.build({ copy({ assets: [{ from: ["./manifest.json", "./main.js", "./styles.css"], - to: ["./test-vault/.obsidian/plugins/simple-time-tracker/."] + to: ["./test-vault/.obsidian/plugins/tag-based-time-tracker/."] }] }), ], diff --git a/package-lock.json b/package-lock.json index 464c6e4..8d9fa98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "simple-time-tracker", + "name": "tag-based-time-tracker", "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "simple-time-tracker", + "name": "tag-based-time-tracker", "version": "1.0.3", "license": "MIT", "devDependencies": { diff --git a/package.json b/package.json index bacd403..5d37640 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "simple-time-tracker", + "name": "tag-based-time-tracker", "version": "1.0.3", "description": "Multi-purpose time trackers for your notes!", "main": "main.js", diff --git a/src/main.ts b/src/main.ts index 81b6b23..e816aea 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { Editor, MarkdownRenderChild, moment, Plugin, TFile } from "obsidian"; -import { defaultSettings, SimpleTimeTrackerSettings } from "./settings"; -import { SimpleTimeTrackerSettingsTab } from "./settings-tab"; +import { defaultSettings, TagBasedTimeTracker } from "./settings"; +import { TagBasedTimeTrackerTab } from "./settings-tab"; import { displayTracker, Entry, @@ -16,7 +16,7 @@ import { } from "./tracker"; import { TimeTrackingSummary } from "./timeTrackingSummary"; -export default class SimpleTimeTrackerPlugin extends Plugin { +export default class TagBasedTimeTrackerPlugin extends Plugin { public api = { // verbatim versions of the functions found in tracker.ts with the same parameters loadTracker, @@ -34,15 +34,15 @@ export default class SimpleTimeTrackerPlugin extends Plugin { orderedEntries: (entries: Entry[]) => orderedEntries(entries, this.settings), }; - public settings: SimpleTimeTrackerSettings; + public settings: TagBasedTimeTracker; async onload(): Promise { await this.loadSettings(); - this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this)); + this.addSettingTab(new TagBasedTimeTrackerTab(this.app, this)); this.registerMarkdownCodeBlockProcessor( - "simple-time-tracker", + "tag-based-time-tracker", (s, e, i) => { e.empty(); let component = new MarkdownRenderChild(e); @@ -77,7 +77,7 @@ export default class SimpleTimeTrackerPlugin extends Plugin { id: `insert`, name: `Insert Time Tracker`, editorCallback: (e, _) => { - e.replaceSelection("```simple-time-tracker\n```\n"); + e.replaceSelection("```tag-based-time-tracker\n```\n"); }, }); diff --git a/src/settings-tab.ts b/src/settings-tab.ts index d683096..a98e70c 100644 --- a/src/settings-tab.ts +++ b/src/settings-tab.ts @@ -1,19 +1,19 @@ import {App, PluginSettingTab, Setting} from "obsidian"; -import SimpleTimeTrackerPlugin from "./main"; +import TagBasedTimeTrackerPlugin from "./main"; import {defaultSettings} from "./settings"; -export class SimpleTimeTrackerSettingsTab extends PluginSettingTab { +export class TagBasedTimeTrackerTab extends PluginSettingTab { - plugin: SimpleTimeTrackerPlugin; + plugin: TagBasedTimeTrackerPlugin; - constructor(app: App, plugin: SimpleTimeTrackerPlugin) { + constructor(app: App, plugin: TagBasedTimeTrackerPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { this.containerEl.empty(); - this.containerEl.createEl("h2", {text: "Super Simple Time Tracker Settings"}); + this.containerEl.createEl("h2", {text: "Tag Based Time Tracker Settings"}); new Setting(this.containerEl) .setName("Timestamp Display Format") @@ -94,12 +94,12 @@ export class SimpleTimeTrackerSettingsTab extends PluginSettingTab { this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" }); this.containerEl.createEl("a", { href: "https://link.ellpeck.de/discordweb" }).createEl("img", { attr: { src: "https://ellpeck.de/res/discord-wide.png" }, - cls: "simple-time-tracker-settings-image" + cls: "tag-based-time-tracker-settings-image" }); this.containerEl.createEl("p", { text: "If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!" }); this.containerEl.createEl("a", { href: "https://ellpeck.de/support" }).createEl("img", { attr: { src: "https://ellpeck.de/res/generalsupport-wide.png" }, - cls: "simple-time-tracker-settings-image" + cls: "tag-based-time-tracker-settings-image" }); } } diff --git a/src/settings.ts b/src/settings.ts index 73295fb..cf39d36 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,4 @@ -export const defaultSettings: SimpleTimeTrackerSettings = { +export const defaultSettings: TagBasedTimeTracker = { timestampFormat: "YY-MM-DD HH:mm:ss", editableTimestampFormat: "YYYY-MM-DD HH:mm:ss", csvDelimiter: ",", @@ -44,7 +44,7 @@ clients: subTags: []` }; -export interface SimpleTimeTrackerSettings { +export interface TagBasedTimeTracker { timestampFormat: string; editableTimestampFormat: string; csvDelimiter: string; diff --git a/src/timeTrackingSummary.ts b/src/timeTrackingSummary.ts index b0a3a8a..fcc603b 100644 --- a/src/timeTrackingSummary.ts +++ b/src/timeTrackingSummary.ts @@ -1,14 +1,14 @@ -import { SimpleTimeTrackerSettings } from "./settings"; +import { TagBasedTimeTracker } from "./settings"; import { Configuration, Section, Item, SubTag } from "./interfaces"; import { parseYaml, moment } from "obsidian"; export class TimeTrackingSummary { - settings: SimpleTimeTrackerSettings; + settings: TagBasedTimeTracker; api: any; app: any; // Reference to the Obsidian app - constructor(app: any, settings: SimpleTimeTrackerSettings, api: any) { + constructor(app: any, settings: TagBasedTimeTracker, api: any) { this.app = app; this.settings = settings; this.api = api; diff --git a/src/tracker.ts b/src/tracker.ts index c55f2ad..587d6eb 100644 --- a/src/tracker.ts +++ b/src/tracker.ts @@ -1,5 +1,5 @@ import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian"; -import { SimpleTimeTrackerSettings } from "./settings"; +import { TagBasedTimeTracker } from "./settings"; import { ConfirmModal } from "./confirm-modal"; export interface Tracker { @@ -51,7 +51,7 @@ export async function loadAllTrackers(fileName: string): Promise<{ section: Mark let curr: Partial | undefined; for (let i = 0; i < content.length; i++) { let line = content[i]; - if (line.trimEnd() == "```simple-time-tracker") { + if (line.trimEnd() == "```tag-based-time-tracker") { curr = { lineStart: i + 1, text: "" }; } else if (curr) { if (line.trimEnd() == "```") { @@ -69,9 +69,9 @@ export async function loadAllTrackers(fileName: string): Promise<{ section: Mark 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: TagBasedTimeTracker, component: MarkdownRenderChild): void { - element.addClass("simple-time-tracker-container"); + element.addClass("tag-based-time-tracker-container"); // add start/stop controls let running = isRunning(tracker); let btn = new ButtonComponent(element) @@ -86,24 +86,24 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: } await saveTracker(tracker, getFile(), getSectionInfo()); }); - btn.buttonEl.addClass("simple-time-tracker-btn"); + btn.buttonEl.addClass("tag-based-time-tracker-btn"); let newSegmentNameBox = new TextComponent(element) .setPlaceholder("Segment name") .setDisabled(running); - newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt"); + newSegmentNameBox.inputEl.addClass("tag-based-time-tracker-txt"); // add timers - let timer = element.createDiv({ cls: "simple-time-tracker-timers" }); - let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" }); - let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" }); + let timer = element.createDiv({ cls: "tag-based-time-tracker-timers" }); + let currentDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let current = currentDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time" }); currentDiv.createEl("span", { text: "Current" }); - let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" }); - let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" }); + let totalDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let total = totalDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time", text: "0s" }); totalDiv.createEl("span", { text: "Total" }); if (tracker.entries.length > 0) { // add table - let table = element.createEl("table", { cls: "simple-time-tracker-table" }); + let table = element.createEl("table", { cls: "tag-based-time-tracker-table" }); table.createEl("tr").append( createEl("th", { text: "Segment" }), createEl("th", { text: "Start time" }), @@ -115,7 +115,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component); // add copy buttons - let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" }); + let buttons = element.createEl("div", { cls: "tag-based-time-tracker-bottom" }); new ButtonComponent(buttons) .setButtonText("Copy as table") .onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings))); @@ -172,7 +172,7 @@ export function getRunningEntry(entries: Entry[]): Entry { return null; } -export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { +export function createMarkdownTable(tracker: Tracker, settings: TagBasedTimeTracker): string { let table = [["Segment", "Start time", "End time", "Duration"]]; for (let entry of orderedEntries(tracker.entries, settings)) table.push(...createTableSection(entry, settings)); @@ -194,7 +194,7 @@ export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTracke return ret; } -export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { +export function createCsv(tracker: Tracker, settings: TagBasedTimeTracker): string { let ret = ""; for (let entry of orderedEntries(tracker.entries, settings)) { for (let row of createTableSection(entry, settings)) @@ -203,15 +203,15 @@ export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings) return ret; } -export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] { +export function orderedEntries(entries: Entry[], settings: TagBasedTimeTracker): Entry[] { return settings.reverseSegmentOrder ? entries.slice().reverse() : entries; } -export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string { +export function formatTimestamp(timestamp: string, settings: TagBasedTimeTracker): string { return moment(timestamp).format(settings.timestampFormat); } -export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string { +export function formatDuration(totalTime: number, settings: TagBasedTimeTracker): string { let ret = ""; let duration = moment.duration(totalTime); let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours()); @@ -289,7 +289,7 @@ function removeEntry(entries: Entry[], toRemove: Entry): boolean { return false; } -function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void { +function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: TagBasedTimeTracker): void { let running = getRunningEntry(tracker.entries); if (running && !running.endTime) { current.setText(formatDuration(getDuration(running), settings)); @@ -300,11 +300,11 @@ function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLE total.setText(formatDuration(getTotalDuration(tracker.entries), settings)); } -function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string { +function formatEditableTimestamp(timestamp: string, settings: TagBasedTimeTracker): string { return moment(timestamp).format(settings.editableTimestampFormat); } -function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string { +function unformatEditableTimestamp(formatted: string, settings: TagBasedTimeTracker): string { return moment(formatted, settings.editableTimestampFormat).toISOString(); } @@ -326,7 +326,7 @@ function updateLegacyInfo(entries: Entry[]): void { } -function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] { +function createTableSection(entry: Entry, settings: TagBasedTimeTracker): string[][] { let ret = [[ entry.name, entry.startTime ? formatTimestamp(entry.startTime, settings) : "", @@ -339,7 +339,7 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): 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: TagBasedTimeTracker, indent: number, component: MarkdownRenderChild): void { let entryRunning = getRunningEntry(tracker.entries) == entry; let row = table.createEl("tr"); @@ -353,7 +353,7 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle let expandButton = new ButtonComponent(nameField.label) .setClass("clickable-icon") - .setClass("simple-time-tracker-expand-button") + .setClass("tag-based-time-tracker-expand-button") .setIcon(`chevron-${entry.collapsed ? "left" : "down"}`) .onClick(async () => { if (entry.collapsed) { @@ -367,7 +367,7 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle expandButton.buttonEl.style.visibility = "hidden"; let entryButtons = row.createEl("td"); - entryButtons.addClass("simple-time-tracker-table-buttons"); + entryButtons.addClass("tag-based-time-tracker-table-buttons"); new ButtonComponent(entryButtons) .setClass("clickable-icon") .setIcon(`lucide-play`) @@ -491,7 +491,7 @@ class EditableField { this.label = this.cell.createEl("span", { text: value }); this.label.style.marginLeft = `${indent}em`; this.box = new TextComponent(this.cell).setValue(value); - this.box.inputEl.addClass("simple-time-tracker-input"); + this.box.inputEl.addClass("tag-based-time-tracker-input"); this.box.inputEl.hide(); this.box.inputEl.addEventListener('keydown', (e: KeyboardEvent) => { // Save with Ctrl/Cmd + Enter @@ -527,9 +527,9 @@ class EditableField { } class EditableTimestampField extends EditableField { - settings: SimpleTimeTrackerSettings; + settings: TagBasedTimeTracker; - constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) { + constructor(row: HTMLTableRowElement, value: string, settings: TagBasedTimeTracker) { super(row, 0, value ? formatTimestamp(value, settings) : ""); this.settings = settings; } diff --git a/styles.css b/styles.css index e066c1f..78588da 100644 --- a/styles.css +++ b/styles.css @@ -1,86 +1,86 @@ -.simple-time-tracker-container { +.tag-based-time-tracker-container { overflow-x: scroll; } -.simple-time-tracker-settings-image { +.tag-based-time-tracker-settings-image { width: 100%; height: auto; } -.simple-time-tracker-btn, -.simple-time-tracker-txt { +.tag-based-time-tracker-btn, +.tag-based-time-tracker-txt { display: block; margin-left: auto; margin-right: auto; } -.simple-time-tracker-txt { +.tag-based-time-tracker-txt { text-align: center; } -.simple-time-tracker-btn { +.tag-based-time-tracker-btn { margin-top: 10px; margin-bottom: 10px; } -.simple-time-tracker-btn svg { +.tag-based-time-tracker-btn svg { width: 32px; height: 32px; } -.simple-time-tracker-bottom button { +.tag-based-time-tracker-bottom button { margin: 10px 5px 10px 5px; } -.simple-time-tracker-timers, -.simple-time-tracker-bottom { +.tag-based-time-tracker-timers, +.tag-based-time-tracker-bottom { display: flex; justify-content: center; text-align: center; } -.simple-time-tracker-timers span { +.tag-based-time-tracker-timers span { display: block; } -.simple-time-tracker-timer { +.tag-based-time-tracker-timer { margin: 20px; } -.simple-time-tracker-timer-time { +.tag-based-time-tracker-timer-time { font-size: xx-large; font-weight: bolder; } -.simple-time-tracker-table { +.tag-based-time-tracker-table { width: 100%; margin-top: 20px; } -.simple-time-tracker-table td, -.simple-time-tracker-table th { +.tag-based-time-tracker-table td, +.tag-based-time-tracker-table th { vertical-align: middle; border: none; } -.simple-time-tracker-table .clickable-icon { +.tag-based-time-tracker-table .clickable-icon { display: inline-block; vertical-align: middle; } -.simple-time-tracker-expand-button { +.tag-based-time-tracker-expand-button { margin-inline-start: 0.5em; } -.simple-time-tracker-input { +.tag-based-time-tracker-input { max-width: 150px; min-width: 100px; } -.simple-time-tracker-table-buttons { +.tag-based-time-tracker-table-buttons { text-align: right !important; } -.simple-time-tracker-table tr:hover { +.tag-based-time-tracker-table tr:hover { background-color: var(--background-modifier-hover); } diff --git a/test-vault/.obsidian/community-plugins.json b/test-vault/.obsidian/community-plugins.json index b1228e8..2ccdf60 100644 --- a/test-vault/.obsidian/community-plugins.json +++ b/test-vault/.obsidian/community-plugins.json @@ -1,4 +1,4 @@ [ "dataview", - "simple-time-tracker" + "tag-based-time-tracker" ] \ No newline at end of file diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/main b/test-vault/.obsidian/plugins/tag-based-time-tracker/main new file mode 100644 index 0000000..3e6a8ca --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/main @@ -0,0 +1,913 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __defProps = Object.defineProperties; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropDescs = Object.getOwnPropertyDescriptors; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); +var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); +var __export = (target, all) => { + __markAsModule(target); + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __reExport = (target, module2, desc) => { + if (module2 && typeof module2 === "object" || typeof module2 === "function") { + for (let key of __getOwnPropNames(module2)) + if (!__hasOwnProp.call(target, key) && key !== "default") + __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable }); + } + return target; +}; +var __toModule = (module2) => { + return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2); +}; +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// src/main.ts +__export(exports, { + default: () => TagBasedTimeTrackerPlugin +}); +var import_obsidian5 = __toModule(require("obsidian")); + +// src/settings.ts +var defaultSettings = { + timestampFormat: "YY-MM-DD HH:mm:ss", + editableTimestampFormat: "YYYY-MM-DD HH:mm:ss", + csvDelimiter: ",", + fineGrainedDurations: true, + reverseSegmentOrder: false, + timestampDurations: false, + tagConfigurationsYaml: ` +# This is a sample configuration file for the tag configurations + +# You can have as many 'sections' as you want to track different domains separately or in parallel + +# Example section 1 +streams: + name: "\u{1F30A} Streams" + items: + - topic: "Accounting" + icon: "\u{1F9EE}" + tag: "#tt_accounting" + subTags: [] + + - topic: "Development" + icon: "\u{1F497}" + tag: "#tt_dev" + subTags: + - topic: "Frontend" + tag: "#tt_frontend" + subTags: [] + + - topic: "Backend" + tag: "#tt_backend" + subTags: [] + +# Example section 2 +clients: + name: "\u{1F468}\u{1F3FC}\u200D\u{1F4BC} Clients" + items: + - topic: "Client A" + tag: "#tt_client_a" + subTags: [] + + - topic: "Client B" + tag: "#tt_client_b" + subTags: []` +}; + +// src/settings-tab.ts +var import_obsidian = __toModule(require("obsidian")); +var TagBasedTimeTrackerTab = class extends import_obsidian.PluginSettingTab { + constructor(app2, plugin) { + super(app2, plugin); + this.plugin = plugin; + } + display() { + this.containerEl.empty(); + this.containerEl.createEl("h2", { text: "Tag Based Time Tracker Settings" }); + new import_obsidian.Setting(this.containerEl).setName("Timestamp Display Format").setDesc(createFragment((f) => { + f.createSpan({ text: "The way that timestamps in time tracker tables should be displayed. Uses " }); + f.createEl("a", { text: "moment.js", href: "https://momentjs.com/docs/#/parsing/string-format/" }); + f.createSpan({ text: " syntax." }); + })).addText((t) => { + t.setValue(String(this.plugin.settings.timestampFormat)); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.timestampFormat = v.length ? v : defaultSettings.timestampFormat; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("CSV Delimiter").setDesc("The delimiter character that should be used when copying a tracker table as CSV. For example, some languages use a semicolon instead of a comma.").addText((t) => { + t.setValue(String(this.plugin.settings.csvDelimiter)); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.csvDelimiter = v.length ? v : defaultSettings.csvDelimiter; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Fine-Grained Durations").setDesc("Whether durations should include days, months and years. If this is disabled, additional time units will be displayed as part of the hours.").addToggle((t) => { + t.setValue(this.plugin.settings.fineGrainedDurations); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.fineGrainedDurations = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Timestamp Durations").setDesc("Whether durations should be displayed in a timestamp format (12:15:01) rather than the default duration format (12h 15m 1s).").addToggle((t) => { + t.setValue(this.plugin.settings.timestampDurations); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.timestampDurations = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Display Segments in Reverse Order").setDesc("Whether older tracker segments should be displayed towards the bottom of the tracker, rather than the top.").addToggle((t) => { + t.setValue(this.plugin.settings.reverseSegmentOrder); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.reverseSegmentOrder = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Tag Configurations").setDesc("Configure your tags, icons, and sub-tags using YAML.").addTextArea((text) => { + text.setPlaceholder("Enter tag configurations in YAML format").setValue(this.plugin.settings.tagConfigurationsYaml).onChange((value) => __async(this, null, function* () { + this.plugin.settings.tagConfigurationsYaml = value; + yield this.plugin.saveSettings(); + })); + text.inputEl.rows = 15; + text.inputEl.style.width = "100%"; + }); + this.containerEl.createEl("hr"); + this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" }); + this.containerEl.createEl("a", { href: "https://link.ellpeck.de/discordweb" }).createEl("img", { + attr: { src: "https://ellpeck.de/res/discord-wide.png" }, + cls: "tag-based-time-tracker-settings-image" + }); + this.containerEl.createEl("p", { text: "If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!" }); + this.containerEl.createEl("a", { href: "https://ellpeck.de/support" }).createEl("img", { + attr: { src: "https://ellpeck.de/res/generalsupport-wide.png" }, + cls: "tag-based-time-tracker-settings-image" + }); + } +}; + +// src/tracker.ts +var import_obsidian3 = __toModule(require("obsidian")); + +// src/confirm-modal.ts +var import_obsidian2 = __toModule(require("obsidian")); +var ConfirmModal = class extends import_obsidian2.Modal { + constructor(app2, message, callback) { + super(app2); + this.message = message; + this.callback = callback; + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("p", { text: this.message }); + new import_obsidian2.Setting(contentEl).addButton((btn) => btn.setButtonText("Ok").setCta().onClick(() => { + this.picked = true; + this.close(); + this.callback(true); + })).addButton((btn) => btn.setButtonText("Cancel").onClick(() => { + this.picked = true; + this.close(); + this.callback(false); + })); + } + onClose() { + if (!this.picked) { + this.callback(false); + } + } +}; + +// src/tracker.ts +function saveTracker(tracker, fileName, section) { + return __async(this, null, function* () { + let file = app.vault.getAbstractFileByPath(fileName); + if (!file) + return; + let content = yield app.vault.read(file); + 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"); + content = `${prev} +${JSON.stringify(tracker)} +${next}`; + yield app.vault.modify(file, content); + }); +} +function loadTracker(json) { + if (json) { + try { + let ret = JSON.parse(json); + updateLegacyInfo(ret.entries); + return ret; + } catch (e) { + console.log(`Failed to parse Tracker from ${json}`); + } + } + return { entries: [] }; +} +function loadAllTrackers(fileName) { + return __async(this, null, function* () { + let file = app.vault.getAbstractFileByPath(fileName); + let content = (yield app.vault.cachedRead(file)).split("\n"); + let trackers = []; + let curr; + for (let i = 0; i < content.length; i++) { + let line = content[i]; + if (line.trimEnd() == "```tag-based-time-tracker") { + curr = { lineStart: i + 1, text: "" }; + } else if (curr) { + if (line.trimEnd() == "```") { + curr.lineEnd = i - 1; + let tracker = loadTracker(curr.text); + trackers.push({ section: curr, tracker }); + curr = void 0; + } else { + curr.text += `${line} +`; + } + } + } + return trackers; + }); +} +function displayTracker(tracker, element, getFile, getSectionInfo, settings, component) { + element.addClass("tag-based-time-tracker-container"); + let running = isRunning(tracker); + let btn = new import_obsidian3.ButtonComponent(element).setClass("clickable-icon").setIcon(`lucide-${running ? "stop" : "play"}-circle`).setTooltip(running ? "End" : "Start").onClick(() => __async(this, null, function* () { + if (running) { + endRunningEntry(tracker); + } else { + startNewEntry(tracker, newSegmentNameBox.getValue()); + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + btn.buttonEl.addClass("tag-based-time-tracker-btn"); + let newSegmentNameBox = new import_obsidian3.TextComponent(element).setPlaceholder("Segment name").setDisabled(running); + newSegmentNameBox.inputEl.addClass("tag-based-time-tracker-txt"); + let timer = element.createDiv({ cls: "tag-based-time-tracker-timers" }); + let currentDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let current = currentDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time" }); + currentDiv.createEl("span", { text: "Current" }); + let totalDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let total = totalDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time", text: "0s" }); + totalDiv.createEl("span", { text: "Total" }); + if (tracker.entries.length > 0) { + let table = element.createEl("table", { cls: "tag-based-time-tracker-table" }); + table.createEl("tr").append(createEl("th", { text: "Segment" }), createEl("th", { text: "Start time" }), createEl("th", { text: "End time" }), createEl("th", { text: "Duration" }), createEl("th")); + for (let entry of orderedEntries(tracker.entries, settings)) + addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component); + let buttons = element.createEl("div", { cls: "tag-based-time-tracker-bottom" }); + new import_obsidian3.ButtonComponent(buttons).setButtonText("Copy as table").onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings))); + new import_obsidian3.ButtonComponent(buttons).setButtonText("Copy as CSV").onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings))); + } + setCountdownValues(tracker, current, total, currentDiv, settings); + let intervalId = window.setInterval(() => { + if (!element.isConnected) { + window.clearInterval(intervalId); + return; + } + setCountdownValues(tracker, current, total, currentDiv, settings); + }, 1e3); +} +function getDuration(entry) { + if (entry.subEntries) { + return getTotalDuration(entry.subEntries); + } else { + let endTime = entry.endTime ? (0, import_obsidian3.moment)(entry.endTime) : (0, import_obsidian3.moment)(); + return endTime.diff((0, import_obsidian3.moment)(entry.startTime)); + } +} +function getTotalDuration(entries) { + let ret = 0; + for (let entry of entries) + ret += getDuration(entry); + return ret; +} +function isRunning(tracker) { + return !!getRunningEntry(tracker.entries); +} +function getRunningEntry(entries) { + for (let entry of entries) { + if (entry.subEntries) { + let running = getRunningEntry(entry.subEntries); + if (running) + return running; + } else { + if (!entry.endTime) + return entry; + } + } + return null; +} +function createMarkdownTable(tracker, settings) { + let table = [["Segment", "Start time", "End time", "Duration"]]; + for (let entry of orderedEntries(tracker.entries, settings)) + table.push(...createTableSection(entry, settings)); + table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]); + let ret = ""; + 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++) { + if (r == 1) + ret += "| " + Array.from(Array(4).keys()).map((i) => "-".repeat(widths[i])).join(" | ") + " |\n"; + let row = []; + for (let i = 0; i < 4; i++) + row.push(table[r][i].padEnd(widths[i], " ")); + ret += "| " + row.join(" | ") + " |\n"; + } + return ret; +} +function createCsv(tracker, settings) { + let ret = ""; + for (let entry of orderedEntries(tracker.entries, settings)) { + for (let row of createTableSection(entry, settings)) + ret += row.join(settings.csvDelimiter) + "\n"; + } + return ret; +} +function orderedEntries(entries, settings) { + return settings.reverseSegmentOrder ? entries.slice().reverse() : entries; +} +function formatTimestamp(timestamp, settings) { + return (0, import_obsidian3.moment)(timestamp).format(settings.timestampFormat); +} +function formatDuration(totalTime, settings) { + let ret = ""; + let duration = import_obsidian3.moment.duration(totalTime); + let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours()); + if (settings.timestampDurations) { + if (settings.fineGrainedDurations) { + let days = Math.floor(duration.asDays()); + if (days > 0) + ret += days + "."; + } + ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`; + } else { + if (settings.fineGrainedDurations) { + let years = Math.floor(duration.asYears()); + if (years > 0) + ret += years + "y "; + if (duration.months() > 0) + ret += duration.months() + "M "; + if (duration.days() > 0) + ret += duration.days() + "d "; + } + if (hours > 0) + ret += hours + "h "; + if (duration.minutes() > 0) + ret += duration.minutes() + "m "; + ret += duration.seconds() + "s"; + } + return ret; +} +function startSubEntry(entry, name) { + if (!entry.subEntries) { + entry.subEntries = [__spreadProps(__spreadValues({}, entry), { name: `Part 1` })]; + entry.startTime = null; + entry.endTime = null; + } + if (!name) + name = `Part ${entry.subEntries.length + 1}`; + entry.subEntries.push({ name, startTime: (0, import_obsidian3.moment)().toISOString(), endTime: null, subEntries: void 0 }); +} +function startNewEntry(tracker, name) { + if (!name) + name = `Segment ${tracker.entries.length + 1}`; + let entry = { name, startTime: (0, import_obsidian3.moment)().toISOString(), endTime: null, subEntries: void 0 }; + tracker.entries.push(entry); +} +function endRunningEntry(tracker) { + let entry = getRunningEntry(tracker.entries); + entry.endTime = (0, import_obsidian3.moment)().toISOString(); +} +function removeEntry(entries, toRemove) { + if (entries.contains(toRemove)) { + entries.remove(toRemove); + return true; + } else { + for (let entry of entries) { + if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) { + if (entry.subEntries.length == 1) { + let single = entry.subEntries[0]; + entry.startTime = single.startTime; + entry.endTime = single.endTime; + entry.subEntries = void 0; + } + return true; + } + } + } + return false; +} +function setCountdownValues(tracker, current, total, currentDiv, settings) { + let running = getRunningEntry(tracker.entries); + if (running && !running.endTime) { + current.setText(formatDuration(getDuration(running), settings)); + currentDiv.hidden = false; + } else { + currentDiv.hidden = true; + } + total.setText(formatDuration(getTotalDuration(tracker.entries), settings)); +} +function formatEditableTimestamp(timestamp, settings) { + return (0, import_obsidian3.moment)(timestamp).format(settings.editableTimestampFormat); +} +function unformatEditableTimestamp(formatted, settings) { + return (0, import_obsidian3.moment)(formatted, settings.editableTimestampFormat).toISOString(); +} +function updateLegacyInfo(entries) { + for (let entry of entries) { + if (entry.startTime && !isNaN(+entry.startTime)) + entry.startTime = import_obsidian3.moment.unix(+entry.startTime).toISOString(); + if (entry.endTime && !isNaN(+entry.endTime)) + entry.endTime = import_obsidian3.moment.unix(+entry.endTime).toISOString(); + if (entry.subEntries == null || !entry.subEntries.length) + entry.subEntries = void 0; + if (entry.subEntries) + updateLegacyInfo(entry.subEntries); + } +} +function createTableSection(entry, settings) { + let ret = [[ + entry.name, + entry.startTime ? formatTimestamp(entry.startTime, settings) : "", + entry.endTime ? formatTimestamp(entry.endTime, settings) : "", + entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" + ]]; + if (entry.subEntries) { + for (let sub of orderedEntries(entry.subEntries, settings)) + ret.push(...createTableSection(sub, settings)); + } + return ret; +} +function addEditableTableRow(tracker, entry, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent, component) { + let entryRunning = getRunningEntry(tracker.entries) == entry; + let row = table.createEl("tr"); + let nameField = new EditableField(row, indent, entry.name); + let startField = new EditableTimestampField(row, entry.startTime, settings); + let endField = new EditableTimestampField(row, entry.endTime, settings); + row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" }); + renderNameAsMarkdown(nameField.label, getFile, component); + let expandButton = new import_obsidian3.ButtonComponent(nameField.label).setClass("clickable-icon").setClass("tag-based-time-tracker-expand-button").setIcon(`chevron-${entry.collapsed ? "left" : "down"}`).onClick(() => __async(this, null, function* () { + if (entry.collapsed) { + entry.collapsed = void 0; + } else { + entry.collapsed = true; + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + if (!entry.subEntries) + expandButton.buttonEl.style.visibility = "hidden"; + let entryButtons = row.createEl("td"); + entryButtons.addClass("tag-based-time-tracker-table-buttons"); + new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setIcon(`lucide-play`).setTooltip("Continue").setDisabled(trackerRunning).onClick(() => __async(this, null, function* () { + startSubEntry(entry, newSegmentNameBox.getValue()); + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + let editButton = new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Edit").setIcon("lucide-pencil").onClick(() => __async(this, null, function* () { + yield handleEdit(); + })); + nameField.label.addEventListener("dblclick", () => __async(this, null, function* () { + if (!nameField.editing()) { + yield handleEdit(); + } + })); + function handleEdit() { + return __async(this, null, function* () { + if (nameField.editing()) { + yield saveChanges(); + } else { + startEditing(); + } + }); + } + function saveChanges() { + return __async(this, null, function* () { + entry.name = nameField.endEdit(); + expandButton.buttonEl.style.display = null; + startField.endEdit(); + entry.startTime = startField.getTimestamp(); + if (!entryRunning) { + endField.endEdit(); + entry.endTime = endField.getTimestamp(); + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + editButton.setIcon("lucide-pencil"); + renderNameAsMarkdown(nameField.label, getFile, component); + }); + } + function startEditing() { + nameField.beginEdit(entry.name); + expandButton.buttonEl.style.display = "none"; + if (!entry.subEntries) { + startField.beginEdit(entry.startTime); + if (!entryRunning) + endField.beginEdit(entry.endTime); + } + editButton.setIcon("lucide-check"); + nameField.onSave = startField.onSave = endField.onSave = () => __async(this, null, function* () { + yield saveChanges(); + }); + nameField.onCancel = startField.onCancel = endField.onCancel = () => { + nameField.endEdit(); + startField.endEdit(); + if (!entryRunning) { + endField.endEdit(); + } + expandButton.buttonEl.style.display = null; + editButton.setIcon("lucide-pencil"); + }; + } + new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Remove").setIcon("lucide-trash").setDisabled(entryRunning).onClick(() => __async(this, null, function* () { + const confirmed = yield showConfirm("Are you sure you want to delete this entry?"); + if (!confirmed) { + return; + } + removeEntry(tracker.entries, entry); + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + if (entry.subEntries && !entry.collapsed) { + for (let sub of orderedEntries(entry.subEntries, settings)) + addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component); + } +} +function showConfirm(message) { + return new Promise((resolve) => { + const modal = new ConfirmModal(app, message, resolve); + modal.open(); + }); +} +function renderNameAsMarkdown(label, getFile, component) { + void import_obsidian3.MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component); + label.innerHTML = label.querySelector("p").innerHTML; +} +var EditableField = class { + constructor(row, indent, value) { + this.cell = row.createEl("td"); + this.label = this.cell.createEl("span", { text: value }); + this.label.style.marginLeft = `${indent}em`; + this.box = new import_obsidian3.TextComponent(this.cell).setValue(value); + this.box.inputEl.addClass("tag-based-time-tracker-input"); + this.box.inputEl.hide(); + this.box.inputEl.addEventListener("keydown", (e) => { + var _a, _b; + if (e.key === "Enter") { + e.preventDefault(); + (_a = this.onSave) == null ? void 0 : _a.call(this); + } + if (e.key === "Escape") { + e.preventDefault(); + (_b = this.onCancel) == null ? void 0 : _b.call(this); + } + }); + } + editing() { + return this.label.hidden; + } + beginEdit(value) { + this.label.hidden = true; + this.box.setValue(value); + this.box.inputEl.show(); + } + endEdit() { + const value = this.box.getValue(); + this.label.setText(value); + this.box.inputEl.hide(); + this.label.hidden = false; + return value; + } +}; +var EditableTimestampField = class extends EditableField { + constructor(row, value, settings) { + super(row, 0, value ? formatTimestamp(value, settings) : ""); + this.settings = settings; + } + beginEdit(value) { + super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : ""); + } + endEdit() { + const value = this.box.getValue(); + let displayValue = value; + if (value) { + const timestamp = unformatEditableTimestamp(value, this.settings); + displayValue = formatTimestamp(timestamp, this.settings); + } + this.label.setText(displayValue); + this.box.inputEl.hide(); + this.label.hidden = false; + return value; + } + getTimestamp() { + if (this.box.getValue()) { + return unformatEditableTimestamp(this.box.getValue(), this.settings); + } else { + return null; + } + } +}; + +// src/timeTrackingSummary.ts +var import_obsidian4 = __toModule(require("obsidian")); +var TimeTrackingSummary = class { + constructor(app2, settings, api) { + this.app = app2; + this.settings = settings; + this.api = api; + } + timeTrackingSummaryForPeriod(containerEl, startDateStr, endDateStr, streamKey) { + return __async(this, null, function* () { + const startDate = (0, import_obsidian4.moment)(startDateStr).startOf("day"); + const endDate = (0, import_obsidian4.moment)(endDateStr).endOf("day"); + let dimensionDurations = {}; + const untrackedSectionName = "Other"; + let config; + try { + config = this.parseConfiguration(); + } catch (error) { + containerEl.createEl("p", { + text: `Error parsing configuration: ${error.message}` + }); + return; + } + const api = this.api; + const files = this.app.vault.getMarkdownFiles(); + for (const file of files) { + const trackers = yield api.loadAllTrackers(file.path); + for (let { tracker } of trackers) { + for (let entry of tracker.entries) { + let entryDate = (0, import_obsidian4.moment)(entry.startTime); + if (!entryDate.isValid() || !entryDate.isBetween(startDate, endDate, null, "[]")) { + continue; + } + let tags = entry.name.match(/#\w+/g) || []; + let duration = api.getDuration(entry); + duration = duration / (1e3 * 60 * 60); + let mappedDimensions = this.mapTagsToDimensions(tags, config); + if (Object.keys(mappedDimensions).length === 0) { + if (!dimensionDurations[untrackedSectionName]) { + dimensionDurations[untrackedSectionName] = {}; + } + if (!dimensionDurations[untrackedSectionName][untrackedSectionName]) { + dimensionDurations[untrackedSectionName][untrackedSectionName] = 0; + } + dimensionDurations[untrackedSectionName][untrackedSectionName] += duration; + } else { + for (let dimension in mappedDimensions) { + if (!dimensionDurations[dimension]) { + dimensionDurations[dimension] = {}; + } + for (let topic of mappedDimensions[dimension]) { + if (!dimensionDurations[dimension][topic]) { + dimensionDurations[dimension][topic] = 0; + } + dimensionDurations[dimension][topic] += duration; + } + } + } + } + } + } + streamKey = streamKey == null ? void 0 : streamKey.replace(/^["']|["']$/g, "").trim().toLowerCase(); + if (streamKey && streamKey.trim() !== "") { + const sectionMapping = {}; + for (const [key, section] of Object.entries(config)) { + sectionMapping[key.toLowerCase()] = section.name; + sectionMapping[section.name.toLowerCase()] = section.name; + } + const filteredDimensions = {}; + for (const dimension in dimensionDurations) { + if (dimension.toLowerCase() === streamKey || Object.keys(sectionMapping).includes(streamKey)) { + const matchedDimension = sectionMapping[streamKey] || dimension; + if (dimensionDurations[matchedDimension]) { + filteredDimensions[matchedDimension] = dimensionDurations[matchedDimension]; + break; + } + } + } + if (Object.keys(filteredDimensions).length === 0) { + containerEl.createEl("p", { + text: `No results found for stream "${streamKey}" in the selected period [${startDate.format("YYYY-MM-DD")} \u2192 ${endDate.format("YYYY-MM-DD")}]` + }); + return; + } + dimensionDurations = filteredDimensions; + } + for (let dimension in dimensionDurations) { + let totalDuration = Object.values(dimensionDurations[dimension]).reduce((sum, hours) => sum + hours, 0); + let totalMs = totalDuration * 60 * 60 * 1e3; + let formattedTotalDuration = api.formatDuration(totalMs); + containerEl.createEl("h2", { + text: `${dimension} \u{1F4C5} [${startDate.format("YYYY-MM-DD")} \u2192 ${endDate.format("YYYY-MM-DD")}] \u23F3Total: ${formattedTotalDuration}` + }); + let tableEl = containerEl.createEl("table"); + let headerRow = tableEl.createEl("tr"); + ["Icon", "Topic", "Total Hours", "Formatted Duration"].forEach((text) => { + headerRow.createEl("th", { text }); + }); + let tableData = Object.entries(dimensionDurations[dimension]).map(([topic, hours]) => { + let durationMs = hours * 60 * 60 * 1e3; + let formattedHours = api.formatDuration(durationMs); + let icon = this.getIconForTag(topic, config); + return { + icon, + topic, + hours: hours.toFixed(2), + formattedHours + }; + }); + tableData.sort((a, b) => parseFloat(b.hours) - parseFloat(a.hours)); + tableData.forEach((row) => { + let rowEl = tableEl.createEl("tr"); + ["icon", "topic", "hours", "formattedHours"].forEach((key) => { + rowEl.createEl("td", { text: row[key] }); + }); + }); + } + }); + } + parseConfiguration() { + const yamlContent = this.settings.tagConfigurationsYaml; + const config = (0, import_obsidian4.parseYaml)(yamlContent); + if (!config || typeof config !== "object") { + throw new Error("Invalid configuration format."); + } + for (const [sectionKey, sectionValue] of Object.entries(config)) { + if (!sectionValue.hasOwnProperty("name") || !sectionValue.hasOwnProperty("items")) { + throw new Error(`Section ${sectionKey} is missing 'name' or 'items' properties.`); + } + const validateItems = (items) => { + for (const item of items) { + if (!item.hasOwnProperty("topic") || !item.hasOwnProperty("tag")) { + throw new Error(`Item ${JSON.stringify(item)} is missing 'topic' or 'tag' properties.`); + } + item.subTags = item.subTags || []; + validateItems(item.subTags); + } + }; + validateItems(sectionValue.items); + } + return config; + } + mapTagsToDimensions(tags, config) { + const mappedDimensions = {}; + const processItems = (items, dimensionName, tagsInDimension) => { + for (const item of items) { + if (tags.includes(item.tag)) { + tagsInDimension.add(item.topic); + } + if (item.subTags && item.subTags.length > 0) { + processItems(item.subTags, dimensionName, tagsInDimension); + } + } + }; + for (const section of Object.values(config)) { + const dimensionName = section.name; + const tagsInDimension = /* @__PURE__ */ new Set(); + processItems(section.items, dimensionName, tagsInDimension); + if (tagsInDimension.size > 0) { + mappedDimensions[dimensionName] = Array.from(tagsInDimension); + } + } + return mappedDimensions; + } + getIconForTag(topic, config) { + let foundIcon = ""; + const searchItems = (items) => { + for (const item of items) { + if (item.topic === topic) { + foundIcon = item.icon || ""; + return true; + } + if (item.subTags && item.subTags.length > 0) { + if (searchItems(item.subTags)) { + return true; + } + } + } + return false; + }; + for (const section of Object.values(config)) { + if (searchItems(section.items)) { + break; + } + } + return foundIcon; + } +}; + +// src/main.ts +var TagBasedTimeTrackerPlugin = class extends import_obsidian5.Plugin { + constructor() { + super(...arguments); + this.api = { + loadTracker, + loadAllTrackers, + getDuration, + getTotalDuration, + getRunningEntry, + isRunning, + formatTimestamp: (timestamp) => formatTimestamp(timestamp, this.settings), + formatDuration: (totalTime) => formatDuration(totalTime, this.settings), + orderedEntries: (entries) => orderedEntries(entries, this.settings) + }; + } + onload() { + return __async(this, null, function* () { + yield this.loadSettings(); + this.addSettingTab(new TagBasedTimeTrackerTab(this.app, this)); + this.registerMarkdownCodeBlockProcessor("tag-based-time-tracker", (s, e, i) => { + e.empty(); + let component = new import_obsidian5.MarkdownRenderChild(e); + let tracker = loadTracker(s); + let filePath = i.sourcePath; + const getFile = () => filePath; + component.registerEvent(this.app.vault.on("rename", (file, oldPath) => { + if (file instanceof import_obsidian5.TFile && oldPath === filePath) { + filePath = file.path; + } + })); + displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component); + i.addChild(component); + }); + this.addCommand({ + id: `insert`, + name: `Insert Time Tracker`, + editorCallback: (e, _) => { + e.replaceSelection("```tag-based-time-tracker\n```\n"); + } + }); + this.addCommand({ + id: `insert-time-tracking-summary`, + name: `Insert Time Tracking Summary`, + editorCallback: (editor) => this.insertTimeTrackingSummarySummary(editor) + }); + this.registerMarkdownCodeBlockProcessor("time-tracking-summary", (source, el, ctx) => __async(this, null, function* () { + const sourceWithoutComments = source.split("\n")[0].replace(/\/\/.*$/g, ""); + const params = sourceWithoutComments.split(",").map((s) => s.trim()); + const [startDateStr, endDateStr, streamKey] = params; + const timeTracking = new TimeTrackingSummary(this.app, this.settings, this.api); + yield timeTracking.timeTrackingSummaryForPeriod(el, startDateStr, endDateStr, streamKey); + })); + }); + } + insertTimeTrackingSummarySummary(editor) { + const now = (0, import_obsidian5.moment)(); + const firstDay = now.clone().startOf("month").format("YYYY-MM-DD"); + const lastDay = now.clone().endOf("month").format("YYYY-MM-DD"); + const snippet = ` +\`\`\`time-tracking-summary + "${firstDay}", "${lastDay}" // Optional: add ", stream_name" to filter by streams section. By default will print all. +\`\`\``; + editor.replaceSelection(snippet); + } + loadSettings() { + return __async(this, null, function* () { + this.settings = Object.assign({}, defaultSettings, yield this.loadData()); + }); + } + saveSettings() { + return __async(this, null, function* () { + yield this.saveData(this.settings); + }); + } +}; diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/main.js b/test-vault/.obsidian/plugins/tag-based-time-tracker/main.js new file mode 100644 index 0000000..3e6a8ca --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/main.js @@ -0,0 +1,913 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __defProps = Object.defineProperties; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropDescs = Object.getOwnPropertyDescriptors; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); +var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); +var __export = (target, all) => { + __markAsModule(target); + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __reExport = (target, module2, desc) => { + if (module2 && typeof module2 === "object" || typeof module2 === "function") { + for (let key of __getOwnPropNames(module2)) + if (!__hasOwnProp.call(target, key) && key !== "default") + __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable }); + } + return target; +}; +var __toModule = (module2) => { + return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2); +}; +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// src/main.ts +__export(exports, { + default: () => TagBasedTimeTrackerPlugin +}); +var import_obsidian5 = __toModule(require("obsidian")); + +// src/settings.ts +var defaultSettings = { + timestampFormat: "YY-MM-DD HH:mm:ss", + editableTimestampFormat: "YYYY-MM-DD HH:mm:ss", + csvDelimiter: ",", + fineGrainedDurations: true, + reverseSegmentOrder: false, + timestampDurations: false, + tagConfigurationsYaml: ` +# This is a sample configuration file for the tag configurations + +# You can have as many 'sections' as you want to track different domains separately or in parallel + +# Example section 1 +streams: + name: "\u{1F30A} Streams" + items: + - topic: "Accounting" + icon: "\u{1F9EE}" + tag: "#tt_accounting" + subTags: [] + + - topic: "Development" + icon: "\u{1F497}" + tag: "#tt_dev" + subTags: + - topic: "Frontend" + tag: "#tt_frontend" + subTags: [] + + - topic: "Backend" + tag: "#tt_backend" + subTags: [] + +# Example section 2 +clients: + name: "\u{1F468}\u{1F3FC}\u200D\u{1F4BC} Clients" + items: + - topic: "Client A" + tag: "#tt_client_a" + subTags: [] + + - topic: "Client B" + tag: "#tt_client_b" + subTags: []` +}; + +// src/settings-tab.ts +var import_obsidian = __toModule(require("obsidian")); +var TagBasedTimeTrackerTab = class extends import_obsidian.PluginSettingTab { + constructor(app2, plugin) { + super(app2, plugin); + this.plugin = plugin; + } + display() { + this.containerEl.empty(); + this.containerEl.createEl("h2", { text: "Tag Based Time Tracker Settings" }); + new import_obsidian.Setting(this.containerEl).setName("Timestamp Display Format").setDesc(createFragment((f) => { + f.createSpan({ text: "The way that timestamps in time tracker tables should be displayed. Uses " }); + f.createEl("a", { text: "moment.js", href: "https://momentjs.com/docs/#/parsing/string-format/" }); + f.createSpan({ text: " syntax." }); + })).addText((t) => { + t.setValue(String(this.plugin.settings.timestampFormat)); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.timestampFormat = v.length ? v : defaultSettings.timestampFormat; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("CSV Delimiter").setDesc("The delimiter character that should be used when copying a tracker table as CSV. For example, some languages use a semicolon instead of a comma.").addText((t) => { + t.setValue(String(this.plugin.settings.csvDelimiter)); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.csvDelimiter = v.length ? v : defaultSettings.csvDelimiter; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Fine-Grained Durations").setDesc("Whether durations should include days, months and years. If this is disabled, additional time units will be displayed as part of the hours.").addToggle((t) => { + t.setValue(this.plugin.settings.fineGrainedDurations); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.fineGrainedDurations = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Timestamp Durations").setDesc("Whether durations should be displayed in a timestamp format (12:15:01) rather than the default duration format (12h 15m 1s).").addToggle((t) => { + t.setValue(this.plugin.settings.timestampDurations); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.timestampDurations = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Display Segments in Reverse Order").setDesc("Whether older tracker segments should be displayed towards the bottom of the tracker, rather than the top.").addToggle((t) => { + t.setValue(this.plugin.settings.reverseSegmentOrder); + t.onChange((v) => __async(this, null, function* () { + this.plugin.settings.reverseSegmentOrder = v; + yield this.plugin.saveSettings(); + })); + }); + new import_obsidian.Setting(this.containerEl).setName("Tag Configurations").setDesc("Configure your tags, icons, and sub-tags using YAML.").addTextArea((text) => { + text.setPlaceholder("Enter tag configurations in YAML format").setValue(this.plugin.settings.tagConfigurationsYaml).onChange((value) => __async(this, null, function* () { + this.plugin.settings.tagConfigurationsYaml = value; + yield this.plugin.saveSettings(); + })); + text.inputEl.rows = 15; + text.inputEl.style.width = "100%"; + }); + this.containerEl.createEl("hr"); + this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" }); + this.containerEl.createEl("a", { href: "https://link.ellpeck.de/discordweb" }).createEl("img", { + attr: { src: "https://ellpeck.de/res/discord-wide.png" }, + cls: "tag-based-time-tracker-settings-image" + }); + this.containerEl.createEl("p", { text: "If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!" }); + this.containerEl.createEl("a", { href: "https://ellpeck.de/support" }).createEl("img", { + attr: { src: "https://ellpeck.de/res/generalsupport-wide.png" }, + cls: "tag-based-time-tracker-settings-image" + }); + } +}; + +// src/tracker.ts +var import_obsidian3 = __toModule(require("obsidian")); + +// src/confirm-modal.ts +var import_obsidian2 = __toModule(require("obsidian")); +var ConfirmModal = class extends import_obsidian2.Modal { + constructor(app2, message, callback) { + super(app2); + this.message = message; + this.callback = callback; + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("p", { text: this.message }); + new import_obsidian2.Setting(contentEl).addButton((btn) => btn.setButtonText("Ok").setCta().onClick(() => { + this.picked = true; + this.close(); + this.callback(true); + })).addButton((btn) => btn.setButtonText("Cancel").onClick(() => { + this.picked = true; + this.close(); + this.callback(false); + })); + } + onClose() { + if (!this.picked) { + this.callback(false); + } + } +}; + +// src/tracker.ts +function saveTracker(tracker, fileName, section) { + return __async(this, null, function* () { + let file = app.vault.getAbstractFileByPath(fileName); + if (!file) + return; + let content = yield app.vault.read(file); + 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"); + content = `${prev} +${JSON.stringify(tracker)} +${next}`; + yield app.vault.modify(file, content); + }); +} +function loadTracker(json) { + if (json) { + try { + let ret = JSON.parse(json); + updateLegacyInfo(ret.entries); + return ret; + } catch (e) { + console.log(`Failed to parse Tracker from ${json}`); + } + } + return { entries: [] }; +} +function loadAllTrackers(fileName) { + return __async(this, null, function* () { + let file = app.vault.getAbstractFileByPath(fileName); + let content = (yield app.vault.cachedRead(file)).split("\n"); + let trackers = []; + let curr; + for (let i = 0; i < content.length; i++) { + let line = content[i]; + if (line.trimEnd() == "```tag-based-time-tracker") { + curr = { lineStart: i + 1, text: "" }; + } else if (curr) { + if (line.trimEnd() == "```") { + curr.lineEnd = i - 1; + let tracker = loadTracker(curr.text); + trackers.push({ section: curr, tracker }); + curr = void 0; + } else { + curr.text += `${line} +`; + } + } + } + return trackers; + }); +} +function displayTracker(tracker, element, getFile, getSectionInfo, settings, component) { + element.addClass("tag-based-time-tracker-container"); + let running = isRunning(tracker); + let btn = new import_obsidian3.ButtonComponent(element).setClass("clickable-icon").setIcon(`lucide-${running ? "stop" : "play"}-circle`).setTooltip(running ? "End" : "Start").onClick(() => __async(this, null, function* () { + if (running) { + endRunningEntry(tracker); + } else { + startNewEntry(tracker, newSegmentNameBox.getValue()); + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + btn.buttonEl.addClass("tag-based-time-tracker-btn"); + let newSegmentNameBox = new import_obsidian3.TextComponent(element).setPlaceholder("Segment name").setDisabled(running); + newSegmentNameBox.inputEl.addClass("tag-based-time-tracker-txt"); + let timer = element.createDiv({ cls: "tag-based-time-tracker-timers" }); + let currentDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let current = currentDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time" }); + currentDiv.createEl("span", { text: "Current" }); + let totalDiv = timer.createEl("div", { cls: "tag-based-time-tracker-timer" }); + let total = totalDiv.createEl("span", { cls: "tag-based-time-tracker-timer-time", text: "0s" }); + totalDiv.createEl("span", { text: "Total" }); + if (tracker.entries.length > 0) { + let table = element.createEl("table", { cls: "tag-based-time-tracker-table" }); + table.createEl("tr").append(createEl("th", { text: "Segment" }), createEl("th", { text: "Start time" }), createEl("th", { text: "End time" }), createEl("th", { text: "Duration" }), createEl("th")); + for (let entry of orderedEntries(tracker.entries, settings)) + addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component); + let buttons = element.createEl("div", { cls: "tag-based-time-tracker-bottom" }); + new import_obsidian3.ButtonComponent(buttons).setButtonText("Copy as table").onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings))); + new import_obsidian3.ButtonComponent(buttons).setButtonText("Copy as CSV").onClick(() => navigator.clipboard.writeText(createCsv(tracker, settings))); + } + setCountdownValues(tracker, current, total, currentDiv, settings); + let intervalId = window.setInterval(() => { + if (!element.isConnected) { + window.clearInterval(intervalId); + return; + } + setCountdownValues(tracker, current, total, currentDiv, settings); + }, 1e3); +} +function getDuration(entry) { + if (entry.subEntries) { + return getTotalDuration(entry.subEntries); + } else { + let endTime = entry.endTime ? (0, import_obsidian3.moment)(entry.endTime) : (0, import_obsidian3.moment)(); + return endTime.diff((0, import_obsidian3.moment)(entry.startTime)); + } +} +function getTotalDuration(entries) { + let ret = 0; + for (let entry of entries) + ret += getDuration(entry); + return ret; +} +function isRunning(tracker) { + return !!getRunningEntry(tracker.entries); +} +function getRunningEntry(entries) { + for (let entry of entries) { + if (entry.subEntries) { + let running = getRunningEntry(entry.subEntries); + if (running) + return running; + } else { + if (!entry.endTime) + return entry; + } + } + return null; +} +function createMarkdownTable(tracker, settings) { + let table = [["Segment", "Start time", "End time", "Duration"]]; + for (let entry of orderedEntries(tracker.entries, settings)) + table.push(...createTableSection(entry, settings)); + table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]); + let ret = ""; + 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++) { + if (r == 1) + ret += "| " + Array.from(Array(4).keys()).map((i) => "-".repeat(widths[i])).join(" | ") + " |\n"; + let row = []; + for (let i = 0; i < 4; i++) + row.push(table[r][i].padEnd(widths[i], " ")); + ret += "| " + row.join(" | ") + " |\n"; + } + return ret; +} +function createCsv(tracker, settings) { + let ret = ""; + for (let entry of orderedEntries(tracker.entries, settings)) { + for (let row of createTableSection(entry, settings)) + ret += row.join(settings.csvDelimiter) + "\n"; + } + return ret; +} +function orderedEntries(entries, settings) { + return settings.reverseSegmentOrder ? entries.slice().reverse() : entries; +} +function formatTimestamp(timestamp, settings) { + return (0, import_obsidian3.moment)(timestamp).format(settings.timestampFormat); +} +function formatDuration(totalTime, settings) { + let ret = ""; + let duration = import_obsidian3.moment.duration(totalTime); + let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours()); + if (settings.timestampDurations) { + if (settings.fineGrainedDurations) { + let days = Math.floor(duration.asDays()); + if (days > 0) + ret += days + "."; + } + ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`; + } else { + if (settings.fineGrainedDurations) { + let years = Math.floor(duration.asYears()); + if (years > 0) + ret += years + "y "; + if (duration.months() > 0) + ret += duration.months() + "M "; + if (duration.days() > 0) + ret += duration.days() + "d "; + } + if (hours > 0) + ret += hours + "h "; + if (duration.minutes() > 0) + ret += duration.minutes() + "m "; + ret += duration.seconds() + "s"; + } + return ret; +} +function startSubEntry(entry, name) { + if (!entry.subEntries) { + entry.subEntries = [__spreadProps(__spreadValues({}, entry), { name: `Part 1` })]; + entry.startTime = null; + entry.endTime = null; + } + if (!name) + name = `Part ${entry.subEntries.length + 1}`; + entry.subEntries.push({ name, startTime: (0, import_obsidian3.moment)().toISOString(), endTime: null, subEntries: void 0 }); +} +function startNewEntry(tracker, name) { + if (!name) + name = `Segment ${tracker.entries.length + 1}`; + let entry = { name, startTime: (0, import_obsidian3.moment)().toISOString(), endTime: null, subEntries: void 0 }; + tracker.entries.push(entry); +} +function endRunningEntry(tracker) { + let entry = getRunningEntry(tracker.entries); + entry.endTime = (0, import_obsidian3.moment)().toISOString(); +} +function removeEntry(entries, toRemove) { + if (entries.contains(toRemove)) { + entries.remove(toRemove); + return true; + } else { + for (let entry of entries) { + if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) { + if (entry.subEntries.length == 1) { + let single = entry.subEntries[0]; + entry.startTime = single.startTime; + entry.endTime = single.endTime; + entry.subEntries = void 0; + } + return true; + } + } + } + return false; +} +function setCountdownValues(tracker, current, total, currentDiv, settings) { + let running = getRunningEntry(tracker.entries); + if (running && !running.endTime) { + current.setText(formatDuration(getDuration(running), settings)); + currentDiv.hidden = false; + } else { + currentDiv.hidden = true; + } + total.setText(formatDuration(getTotalDuration(tracker.entries), settings)); +} +function formatEditableTimestamp(timestamp, settings) { + return (0, import_obsidian3.moment)(timestamp).format(settings.editableTimestampFormat); +} +function unformatEditableTimestamp(formatted, settings) { + return (0, import_obsidian3.moment)(formatted, settings.editableTimestampFormat).toISOString(); +} +function updateLegacyInfo(entries) { + for (let entry of entries) { + if (entry.startTime && !isNaN(+entry.startTime)) + entry.startTime = import_obsidian3.moment.unix(+entry.startTime).toISOString(); + if (entry.endTime && !isNaN(+entry.endTime)) + entry.endTime = import_obsidian3.moment.unix(+entry.endTime).toISOString(); + if (entry.subEntries == null || !entry.subEntries.length) + entry.subEntries = void 0; + if (entry.subEntries) + updateLegacyInfo(entry.subEntries); + } +} +function createTableSection(entry, settings) { + let ret = [[ + entry.name, + entry.startTime ? formatTimestamp(entry.startTime, settings) : "", + entry.endTime ? formatTimestamp(entry.endTime, settings) : "", + entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" + ]]; + if (entry.subEntries) { + for (let sub of orderedEntries(entry.subEntries, settings)) + ret.push(...createTableSection(sub, settings)); + } + return ret; +} +function addEditableTableRow(tracker, entry, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent, component) { + let entryRunning = getRunningEntry(tracker.entries) == entry; + let row = table.createEl("tr"); + let nameField = new EditableField(row, indent, entry.name); + let startField = new EditableTimestampField(row, entry.startTime, settings); + let endField = new EditableTimestampField(row, entry.endTime, settings); + row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" }); + renderNameAsMarkdown(nameField.label, getFile, component); + let expandButton = new import_obsidian3.ButtonComponent(nameField.label).setClass("clickable-icon").setClass("tag-based-time-tracker-expand-button").setIcon(`chevron-${entry.collapsed ? "left" : "down"}`).onClick(() => __async(this, null, function* () { + if (entry.collapsed) { + entry.collapsed = void 0; + } else { + entry.collapsed = true; + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + if (!entry.subEntries) + expandButton.buttonEl.style.visibility = "hidden"; + let entryButtons = row.createEl("td"); + entryButtons.addClass("tag-based-time-tracker-table-buttons"); + new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setIcon(`lucide-play`).setTooltip("Continue").setDisabled(trackerRunning).onClick(() => __async(this, null, function* () { + startSubEntry(entry, newSegmentNameBox.getValue()); + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + let editButton = new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Edit").setIcon("lucide-pencil").onClick(() => __async(this, null, function* () { + yield handleEdit(); + })); + nameField.label.addEventListener("dblclick", () => __async(this, null, function* () { + if (!nameField.editing()) { + yield handleEdit(); + } + })); + function handleEdit() { + return __async(this, null, function* () { + if (nameField.editing()) { + yield saveChanges(); + } else { + startEditing(); + } + }); + } + function saveChanges() { + return __async(this, null, function* () { + entry.name = nameField.endEdit(); + expandButton.buttonEl.style.display = null; + startField.endEdit(); + entry.startTime = startField.getTimestamp(); + if (!entryRunning) { + endField.endEdit(); + entry.endTime = endField.getTimestamp(); + } + yield saveTracker(tracker, getFile(), getSectionInfo()); + editButton.setIcon("lucide-pencil"); + renderNameAsMarkdown(nameField.label, getFile, component); + }); + } + function startEditing() { + nameField.beginEdit(entry.name); + expandButton.buttonEl.style.display = "none"; + if (!entry.subEntries) { + startField.beginEdit(entry.startTime); + if (!entryRunning) + endField.beginEdit(entry.endTime); + } + editButton.setIcon("lucide-check"); + nameField.onSave = startField.onSave = endField.onSave = () => __async(this, null, function* () { + yield saveChanges(); + }); + nameField.onCancel = startField.onCancel = endField.onCancel = () => { + nameField.endEdit(); + startField.endEdit(); + if (!entryRunning) { + endField.endEdit(); + } + expandButton.buttonEl.style.display = null; + editButton.setIcon("lucide-pencil"); + }; + } + new import_obsidian3.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Remove").setIcon("lucide-trash").setDisabled(entryRunning).onClick(() => __async(this, null, function* () { + const confirmed = yield showConfirm("Are you sure you want to delete this entry?"); + if (!confirmed) { + return; + } + removeEntry(tracker.entries, entry); + yield saveTracker(tracker, getFile(), getSectionInfo()); + })); + if (entry.subEntries && !entry.collapsed) { + for (let sub of orderedEntries(entry.subEntries, settings)) + addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component); + } +} +function showConfirm(message) { + return new Promise((resolve) => { + const modal = new ConfirmModal(app, message, resolve); + modal.open(); + }); +} +function renderNameAsMarkdown(label, getFile, component) { + void import_obsidian3.MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component); + label.innerHTML = label.querySelector("p").innerHTML; +} +var EditableField = class { + constructor(row, indent, value) { + this.cell = row.createEl("td"); + this.label = this.cell.createEl("span", { text: value }); + this.label.style.marginLeft = `${indent}em`; + this.box = new import_obsidian3.TextComponent(this.cell).setValue(value); + this.box.inputEl.addClass("tag-based-time-tracker-input"); + this.box.inputEl.hide(); + this.box.inputEl.addEventListener("keydown", (e) => { + var _a, _b; + if (e.key === "Enter") { + e.preventDefault(); + (_a = this.onSave) == null ? void 0 : _a.call(this); + } + if (e.key === "Escape") { + e.preventDefault(); + (_b = this.onCancel) == null ? void 0 : _b.call(this); + } + }); + } + editing() { + return this.label.hidden; + } + beginEdit(value) { + this.label.hidden = true; + this.box.setValue(value); + this.box.inputEl.show(); + } + endEdit() { + const value = this.box.getValue(); + this.label.setText(value); + this.box.inputEl.hide(); + this.label.hidden = false; + return value; + } +}; +var EditableTimestampField = class extends EditableField { + constructor(row, value, settings) { + super(row, 0, value ? formatTimestamp(value, settings) : ""); + this.settings = settings; + } + beginEdit(value) { + super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : ""); + } + endEdit() { + const value = this.box.getValue(); + let displayValue = value; + if (value) { + const timestamp = unformatEditableTimestamp(value, this.settings); + displayValue = formatTimestamp(timestamp, this.settings); + } + this.label.setText(displayValue); + this.box.inputEl.hide(); + this.label.hidden = false; + return value; + } + getTimestamp() { + if (this.box.getValue()) { + return unformatEditableTimestamp(this.box.getValue(), this.settings); + } else { + return null; + } + } +}; + +// src/timeTrackingSummary.ts +var import_obsidian4 = __toModule(require("obsidian")); +var TimeTrackingSummary = class { + constructor(app2, settings, api) { + this.app = app2; + this.settings = settings; + this.api = api; + } + timeTrackingSummaryForPeriod(containerEl, startDateStr, endDateStr, streamKey) { + return __async(this, null, function* () { + const startDate = (0, import_obsidian4.moment)(startDateStr).startOf("day"); + const endDate = (0, import_obsidian4.moment)(endDateStr).endOf("day"); + let dimensionDurations = {}; + const untrackedSectionName = "Other"; + let config; + try { + config = this.parseConfiguration(); + } catch (error) { + containerEl.createEl("p", { + text: `Error parsing configuration: ${error.message}` + }); + return; + } + const api = this.api; + const files = this.app.vault.getMarkdownFiles(); + for (const file of files) { + const trackers = yield api.loadAllTrackers(file.path); + for (let { tracker } of trackers) { + for (let entry of tracker.entries) { + let entryDate = (0, import_obsidian4.moment)(entry.startTime); + if (!entryDate.isValid() || !entryDate.isBetween(startDate, endDate, null, "[]")) { + continue; + } + let tags = entry.name.match(/#\w+/g) || []; + let duration = api.getDuration(entry); + duration = duration / (1e3 * 60 * 60); + let mappedDimensions = this.mapTagsToDimensions(tags, config); + if (Object.keys(mappedDimensions).length === 0) { + if (!dimensionDurations[untrackedSectionName]) { + dimensionDurations[untrackedSectionName] = {}; + } + if (!dimensionDurations[untrackedSectionName][untrackedSectionName]) { + dimensionDurations[untrackedSectionName][untrackedSectionName] = 0; + } + dimensionDurations[untrackedSectionName][untrackedSectionName] += duration; + } else { + for (let dimension in mappedDimensions) { + if (!dimensionDurations[dimension]) { + dimensionDurations[dimension] = {}; + } + for (let topic of mappedDimensions[dimension]) { + if (!dimensionDurations[dimension][topic]) { + dimensionDurations[dimension][topic] = 0; + } + dimensionDurations[dimension][topic] += duration; + } + } + } + } + } + } + streamKey = streamKey == null ? void 0 : streamKey.replace(/^["']|["']$/g, "").trim().toLowerCase(); + if (streamKey && streamKey.trim() !== "") { + const sectionMapping = {}; + for (const [key, section] of Object.entries(config)) { + sectionMapping[key.toLowerCase()] = section.name; + sectionMapping[section.name.toLowerCase()] = section.name; + } + const filteredDimensions = {}; + for (const dimension in dimensionDurations) { + if (dimension.toLowerCase() === streamKey || Object.keys(sectionMapping).includes(streamKey)) { + const matchedDimension = sectionMapping[streamKey] || dimension; + if (dimensionDurations[matchedDimension]) { + filteredDimensions[matchedDimension] = dimensionDurations[matchedDimension]; + break; + } + } + } + if (Object.keys(filteredDimensions).length === 0) { + containerEl.createEl("p", { + text: `No results found for stream "${streamKey}" in the selected period [${startDate.format("YYYY-MM-DD")} \u2192 ${endDate.format("YYYY-MM-DD")}]` + }); + return; + } + dimensionDurations = filteredDimensions; + } + for (let dimension in dimensionDurations) { + let totalDuration = Object.values(dimensionDurations[dimension]).reduce((sum, hours) => sum + hours, 0); + let totalMs = totalDuration * 60 * 60 * 1e3; + let formattedTotalDuration = api.formatDuration(totalMs); + containerEl.createEl("h2", { + text: `${dimension} \u{1F4C5} [${startDate.format("YYYY-MM-DD")} \u2192 ${endDate.format("YYYY-MM-DD")}] \u23F3Total: ${formattedTotalDuration}` + }); + let tableEl = containerEl.createEl("table"); + let headerRow = tableEl.createEl("tr"); + ["Icon", "Topic", "Total Hours", "Formatted Duration"].forEach((text) => { + headerRow.createEl("th", { text }); + }); + let tableData = Object.entries(dimensionDurations[dimension]).map(([topic, hours]) => { + let durationMs = hours * 60 * 60 * 1e3; + let formattedHours = api.formatDuration(durationMs); + let icon = this.getIconForTag(topic, config); + return { + icon, + topic, + hours: hours.toFixed(2), + formattedHours + }; + }); + tableData.sort((a, b) => parseFloat(b.hours) - parseFloat(a.hours)); + tableData.forEach((row) => { + let rowEl = tableEl.createEl("tr"); + ["icon", "topic", "hours", "formattedHours"].forEach((key) => { + rowEl.createEl("td", { text: row[key] }); + }); + }); + } + }); + } + parseConfiguration() { + const yamlContent = this.settings.tagConfigurationsYaml; + const config = (0, import_obsidian4.parseYaml)(yamlContent); + if (!config || typeof config !== "object") { + throw new Error("Invalid configuration format."); + } + for (const [sectionKey, sectionValue] of Object.entries(config)) { + if (!sectionValue.hasOwnProperty("name") || !sectionValue.hasOwnProperty("items")) { + throw new Error(`Section ${sectionKey} is missing 'name' or 'items' properties.`); + } + const validateItems = (items) => { + for (const item of items) { + if (!item.hasOwnProperty("topic") || !item.hasOwnProperty("tag")) { + throw new Error(`Item ${JSON.stringify(item)} is missing 'topic' or 'tag' properties.`); + } + item.subTags = item.subTags || []; + validateItems(item.subTags); + } + }; + validateItems(sectionValue.items); + } + return config; + } + mapTagsToDimensions(tags, config) { + const mappedDimensions = {}; + const processItems = (items, dimensionName, tagsInDimension) => { + for (const item of items) { + if (tags.includes(item.tag)) { + tagsInDimension.add(item.topic); + } + if (item.subTags && item.subTags.length > 0) { + processItems(item.subTags, dimensionName, tagsInDimension); + } + } + }; + for (const section of Object.values(config)) { + const dimensionName = section.name; + const tagsInDimension = /* @__PURE__ */ new Set(); + processItems(section.items, dimensionName, tagsInDimension); + if (tagsInDimension.size > 0) { + mappedDimensions[dimensionName] = Array.from(tagsInDimension); + } + } + return mappedDimensions; + } + getIconForTag(topic, config) { + let foundIcon = ""; + const searchItems = (items) => { + for (const item of items) { + if (item.topic === topic) { + foundIcon = item.icon || ""; + return true; + } + if (item.subTags && item.subTags.length > 0) { + if (searchItems(item.subTags)) { + return true; + } + } + } + return false; + }; + for (const section of Object.values(config)) { + if (searchItems(section.items)) { + break; + } + } + return foundIcon; + } +}; + +// src/main.ts +var TagBasedTimeTrackerPlugin = class extends import_obsidian5.Plugin { + constructor() { + super(...arguments); + this.api = { + loadTracker, + loadAllTrackers, + getDuration, + getTotalDuration, + getRunningEntry, + isRunning, + formatTimestamp: (timestamp) => formatTimestamp(timestamp, this.settings), + formatDuration: (totalTime) => formatDuration(totalTime, this.settings), + orderedEntries: (entries) => orderedEntries(entries, this.settings) + }; + } + onload() { + return __async(this, null, function* () { + yield this.loadSettings(); + this.addSettingTab(new TagBasedTimeTrackerTab(this.app, this)); + this.registerMarkdownCodeBlockProcessor("tag-based-time-tracker", (s, e, i) => { + e.empty(); + let component = new import_obsidian5.MarkdownRenderChild(e); + let tracker = loadTracker(s); + let filePath = i.sourcePath; + const getFile = () => filePath; + component.registerEvent(this.app.vault.on("rename", (file, oldPath) => { + if (file instanceof import_obsidian5.TFile && oldPath === filePath) { + filePath = file.path; + } + })); + displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component); + i.addChild(component); + }); + this.addCommand({ + id: `insert`, + name: `Insert Time Tracker`, + editorCallback: (e, _) => { + e.replaceSelection("```tag-based-time-tracker\n```\n"); + } + }); + this.addCommand({ + id: `insert-time-tracking-summary`, + name: `Insert Time Tracking Summary`, + editorCallback: (editor) => this.insertTimeTrackingSummarySummary(editor) + }); + this.registerMarkdownCodeBlockProcessor("time-tracking-summary", (source, el, ctx) => __async(this, null, function* () { + const sourceWithoutComments = source.split("\n")[0].replace(/\/\/.*$/g, ""); + const params = sourceWithoutComments.split(",").map((s) => s.trim()); + const [startDateStr, endDateStr, streamKey] = params; + const timeTracking = new TimeTrackingSummary(this.app, this.settings, this.api); + yield timeTracking.timeTrackingSummaryForPeriod(el, startDateStr, endDateStr, streamKey); + })); + }); + } + insertTimeTrackingSummarySummary(editor) { + const now = (0, import_obsidian5.moment)(); + const firstDay = now.clone().startOf("month").format("YYYY-MM-DD"); + const lastDay = now.clone().endOf("month").format("YYYY-MM-DD"); + const snippet = ` +\`\`\`time-tracking-summary + "${firstDay}", "${lastDay}" // Optional: add ", stream_name" to filter by streams section. By default will print all. +\`\`\``; + editor.replaceSelection(snippet); + } + loadSettings() { + return __async(this, null, function* () { + this.settings = Object.assign({}, defaultSettings, yield this.loadData()); + }); + } + saveSettings() { + return __async(this, null, function* () { + yield this.saveData(this.settings); + }); + } +}; diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest b/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest new file mode 100644 index 0000000..dba2d89 --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest @@ -0,0 +1,10 @@ +{ + "id": "tag-based-time-tracker", + "name": "Tag Based Time Tracker", + "version": "1.0.1", + "minAppVersion": "1.2.8", + "description": "Tag based time tracker for your notes!", + "author": "Sergey Novikov", + "authorUrl": "https://github.com/serrnovik", + "isDesktopOnly": false +} diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest.json b/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest.json new file mode 100644 index 0000000..dba2d89 --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "tag-based-time-tracker", + "name": "Tag Based Time Tracker", + "version": "1.0.1", + "minAppVersion": "1.2.8", + "description": "Tag based time tracker for your notes!", + "author": "Sergey Novikov", + "authorUrl": "https://github.com/serrnovik", + "isDesktopOnly": false +} diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/styles b/test-vault/.obsidian/plugins/tag-based-time-tracker/styles new file mode 100644 index 0000000..78588da --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/styles @@ -0,0 +1,86 @@ +.tag-based-time-tracker-container { + overflow-x: scroll; +} + +.tag-based-time-tracker-settings-image { + width: 100%; + height: auto; +} + +.tag-based-time-tracker-btn, +.tag-based-time-tracker-txt { + display: block; + margin-left: auto; + margin-right: auto; +} + +.tag-based-time-tracker-txt { + text-align: center; +} + +.tag-based-time-tracker-btn { + margin-top: 10px; + margin-bottom: 10px; +} + +.tag-based-time-tracker-btn svg { + width: 32px; + height: 32px; +} + +.tag-based-time-tracker-bottom button { + margin: 10px 5px 10px 5px; +} + +.tag-based-time-tracker-timers, +.tag-based-time-tracker-bottom { + display: flex; + justify-content: center; + text-align: center; +} + +.tag-based-time-tracker-timers span { + display: block; +} + +.tag-based-time-tracker-timer { + margin: 20px; +} + +.tag-based-time-tracker-timer-time { + font-size: xx-large; + font-weight: bolder; +} + +.tag-based-time-tracker-table { + width: 100%; + margin-top: 20px; +} + +.tag-based-time-tracker-table td, +.tag-based-time-tracker-table th { + vertical-align: middle; + border: none; +} + +.tag-based-time-tracker-table .clickable-icon { + display: inline-block; + vertical-align: middle; +} + +.tag-based-time-tracker-expand-button { + margin-inline-start: 0.5em; +} + +.tag-based-time-tracker-input { + max-width: 150px; + min-width: 100px; +} + +.tag-based-time-tracker-table-buttons { + text-align: right !important; +} + +.tag-based-time-tracker-table tr:hover { + background-color: var(--background-modifier-hover); +} diff --git a/test-vault/.obsidian/plugins/tag-based-time-tracker/styles.css b/test-vault/.obsidian/plugins/tag-based-time-tracker/styles.css new file mode 100644 index 0000000..78588da --- /dev/null +++ b/test-vault/.obsidian/plugins/tag-based-time-tracker/styles.css @@ -0,0 +1,86 @@ +.tag-based-time-tracker-container { + overflow-x: scroll; +} + +.tag-based-time-tracker-settings-image { + width: 100%; + height: auto; +} + +.tag-based-time-tracker-btn, +.tag-based-time-tracker-txt { + display: block; + margin-left: auto; + margin-right: auto; +} + +.tag-based-time-tracker-txt { + text-align: center; +} + +.tag-based-time-tracker-btn { + margin-top: 10px; + margin-bottom: 10px; +} + +.tag-based-time-tracker-btn svg { + width: 32px; + height: 32px; +} + +.tag-based-time-tracker-bottom button { + margin: 10px 5px 10px 5px; +} + +.tag-based-time-tracker-timers, +.tag-based-time-tracker-bottom { + display: flex; + justify-content: center; + text-align: center; +} + +.tag-based-time-tracker-timers span { + display: block; +} + +.tag-based-time-tracker-timer { + margin: 20px; +} + +.tag-based-time-tracker-timer-time { + font-size: xx-large; + font-weight: bolder; +} + +.tag-based-time-tracker-table { + width: 100%; + margin-top: 20px; +} + +.tag-based-time-tracker-table td, +.tag-based-time-tracker-table th { + vertical-align: middle; + border: none; +} + +.tag-based-time-tracker-table .clickable-icon { + display: inline-block; + vertical-align: middle; +} + +.tag-based-time-tracker-expand-button { + margin-inline-start: 0.5em; +} + +.tag-based-time-tracker-input { + max-width: 150px; + min-width: 100px; +} + +.tag-based-time-tracker-table-buttons { + text-align: right !important; +} + +.tag-based-time-tracker-table tr:hover { + background-color: var(--background-modifier-hover); +} diff --git a/test-vault/Time summary reporting test.md b/test-vault/Time summary reporting test.md index 545d09a..c19b555 100644 --- a/test-vault/Time summary reporting test.md +++ b/test-vault/Time summary reporting test.md @@ -1,6 +1,6 @@ # Example data -```simple-time-tracker +```tag-based-time-tracker {"entries":[{"name":"#tt_accounting #tt_client_a bills processing","startTime":"2024-11-01T09:25:42.000Z","endTime":"2024-11-01T17:25:56.000Z"},{"name":"#tt_dev #tt_client2 ","startTime":"2024-11-02T13:26:21.000Z","endTime":"2024-11-02T17:27:18.000Z"},{"name":"#tt_dev #tt_client_a #tt_frontend did this","startTime":"2024-11-03T08:27:20.000Z","endTime":"2024-11-03T17:27:39.000Z"},{"name":"#tt_dev #tt_client_b #tt_backend did that","startTime":"2024-11-04T14:27:40.000Z","endTime":"2024-11-04T17:27:53.000Z"}]} ``` @@ -14,4 +14,4 @@ ```time-tracking-summary "2024-11-01", "2024-11-30", clients // Optional: add ", stream_name" to filter by streams section. By default will print all. -``` \ No newline at end of file +``` diff --git a/test-vault/dataview-test.md b/test-vault/dataview-test.md index 56d0aa6..6b17d16 100644 --- a/test-vault/dataview-test.md +++ b/test-vault/dataview-test.md @@ -1,6 +1,6 @@ ```dataviewjs // get the time tracker plugin api instance -let api = dv.app.plugins.plugins["simple-time-tracker"].api; +let api = dv.app.plugins.plugins["tag-based-time-tracker"].api; for(let page of dv.pages()) { // load trackers in the file with the given path diff --git a/test-vault/duration_accumulation_test.md b/test-vault/duration_accumulation_test.md index a289167..fedb8d3 100644 --- a/test-vault/duration_accumulation_test.md +++ b/test-vault/duration_accumulation_test.md @@ -1,10 +1,10 @@ # Notes More notes for my cool project! This note shows that we can correctly display accumulated time that lasts longer than a 24 hour day! -```simple-time-tracker +```tag-based-time-tracker {"entries":[{"name":"test","startTime":"2020-08-01T07:00:00.000Z","endTime":"2021-08-02T07:00:00.000Z","subEntries":null},{"name":"test","startTime":"2021-08-01T07:00:00.000Z","endTime":"2021-10-02T07:00:00.000Z","subEntries":null},{"name":"test","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2022-08-01T07:00:00.000Z","endTime":"2022-08-02T07:00:00.000Z","subEntries":null},{"name":"Part 2","startTime":"2024-02-26T13:19:40.629Z","endTime":"2024-02-26T13:19:43.713Z","subEntries":null},{"name":"Part 3","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-02-26T13:23:51.939Z","endTime":"2024-02-26T13:23:54.232Z","subEntries":null},{"name":"Part 2","startTime":"2024-02-26T13:27:34.397Z","endTime":"2024-02-26T13:27:49.282Z","subEntries":null}]},{"name":"Part 4","startTime":"2024-02-26T13:29:06.983Z","endTime":"2024-02-26T13:29:20.770Z","subEntries":null}]},{"name":"test","startTime":"2022-10-01T12:30:10.000Z","endTime":"2022-10-01T13:40:05.000Z","subEntries":null},{"name":"Segment 5","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2023-05-23T16:16:56.000Z","endTime":"2023-05-23T16:16:59.000Z","subEntries":null},{"name":"Part 2","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-02-26T13:30:39.632Z","endTime":"2024-02-26T13:30:56.290Z","subEntries":null},{"name":"Part 2","startTime":"2024-02-26T13:30:57.000Z","endTime":"2024-02-26T13:31:00.000Z","subEntries":null}]},{"name":"Part 3","startTime":"2024-02-26T13:34:18.537Z","endTime":"2024-02-26T13:34:21.169Z","subEntries":null}]}]} ``` -```simple-time-tracker +```tag-based-time-tracker {"entries":[{"name":"Segment 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-02-26T13:37:59.292Z","endTime":"2024-02-26T13:38:01.437Z","subEntries":null},{"name":"Part 2","startTime":"2024-02-26T14:04:14.156Z","endTime":"2024-02-26T14:04:30.576Z","subEntries":null}]},{"name":"Part 2","startTime":"2024-02-26T13:38:16.235Z","endTime":"2024-02-26T13:38:18.895Z","subEntries":null}]}]} ``` diff --git a/test-vault/test-markdown.md b/test-vault/test-markdown.md index cca48ec..d61c76a 100644 --- a/test-vault/test-markdown.md +++ b/test-vault/test-markdown.md @@ -1,5 +1,5 @@ Tested for #tag, *italic*, [link](test2), etc: -```simple-time-tracker +```tag-based-time-tracker {"entries":[{"name":"`Segment 1`","startTime":"2022-09-27T19:51:18.000Z","endTime":"2022-09-27T19:51:24.000Z"},{"name":"Segment 2","startTime":"2022-09-27T19:51:25.000Z","endTime":"2022-09-27T19:51:26.000Z"},{"name":"#tag Seqment 3 *add* #tag1 text","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1 #tagp1","startTime":"2024-03-17T11:16:00.382Z","endTime":"2024-03-17T11:16:15.966Z"},{"name":"Part 3","startTime":"2024-03-17T11:17:08.000Z","endTime":"2024-03-17T11:17:24.000Z"}]},{"name":"#tag3 Segment 4","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1 #tag4","startTime":"2024-03-17T12:22:04.000Z","endTime":"2024-03-17T12:22:16.000Z"},{"name":"#tag5 Part 2 *italic*","startTime":"2024-03-17T12:22:20.000Z","endTime":"2024-03-17T12:22:24.000Z"}]},{"name":"*italic* Segment 5 #tag6 [test2](test2)","startTime":"2024-03-17T12:40:37.000Z","endTime":"2024-03-17T12:40:45.000Z"},{"name":"Segment 6","startTime":"2024-03-27T13:20:56.000Z","endTime":"2024-08-09T16:27:18.029Z"}]} ``` diff --git a/test-vault/test/Cool Project.md b/test-vault/test/Cool Project.md index c255f35..6a2ab42 100644 --- a/test-vault/test/Cool Project.md +++ b/test-vault/test/Cool Project.md @@ -1,6 +1,6 @@ # Notes 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 +```tag-based-time-tracker {"entries":[{"name":"Segment 1","startTime":"2022-10-19T14:32:28.000Z","endTime":"2022-10-19T14:32:31.000Z","subEntries":null},{"name":"Segment 2","startTime":"2022-10-19T14:32:33.000Z","endTime":"2022-10-19T14:32:41.000Z","subEntries":null},{"name":"Segment 3","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2022-10-19T14:32:42.000Z","endTime":"2022-10-19T14:33:15.000Z","subEntries":null},{"name":"Part 2","startTime":"2022-10-19T14:33:24.000Z","endTime":"2022-10-19T14:33:45.000Z","subEntries":null},{"name":"Part 3","startTime":"2022-10-19T14:34:54.000Z","endTime":"2022-10-19T14:35:01.000Z","subEntries":null}]},{"name":"Segment 4","startTime":"2022-10-19T14:34:48.000Z","endTime":"2022-10-19T14:34:51.000Z","subEntries":null},{"name":"Segment 5","startTime":"2023-05-23T16:01:44.000Z","endTime":"2023-05-23T16:01:48.000Z","subEntries":null},{"name":"Segment 6","startTime":"2023-05-23T16:01:50.000Z","endTime":"2023-05-23T16:01:52.000Z","subEntries":null},{"name":"Segment 7","startTime":"2023-05-23T16:02:09.000Z","endTime":"2023-05-23T16:02:12.000Z","subEntries":null},{"name":"Segment 8","startTime":"1970-01-01T00:00:00.000Z","endTime":null,"subEntries":[{"name":"Part 1","startTime":"2023-05-23T16:02:20.000Z","endTime":"2023-05-23T16:02:28.000Z","subEntries":null},{"name":"Part 2","startTime":"2023-09-08T11:58:11.000Z","endTime":"2023-09-08T11:58:38.000Z","subEntries":null}]},{"name":"Segment 9","startTime":"2023-09-08T12:00:35.000Z","endTime":"2023-09-08T12:00:49.991Z","subEntries":null},{"name":"Segment 10","startTime":"2023-09-08T12:01:45.000Z","endTime":"2023-09-08T12:01:53.711Z","subEntries":null}]} ``` diff --git a/test-vault/test2.md b/test-vault/test2.md deleted file mode 100644 index 4f614db..0000000 --- a/test-vault/test2.md +++ /dev/null @@ -1,3 +0,0 @@ -```simple-time-tracker -{"entries":[{"name":"Segment 1","startTime":1664308278,"endTime":1664308284},{"name":"Segment 2","startTime":1664308285,"endTime":1664308286},{"name":"Segment 3","startTime":1664308299,"endTime":1664308312}]} -``` diff --git a/test-vault/track-note-test.md b/test-vault/track-note-test.md index 15fb6a3..0c03576 100644 --- a/test-vault/track-note-test.md +++ b/test-vault/track-note-test.md @@ -1,6 +1,6 @@ This is a time tracker: -```simple-time-tracker +```tag-based-time-tracker {"entries":[{"name":"Segment 1","startTime":1664306406,"endTime":1664306408},{"name":"Segment 2","startTime":1664306409,"endTime":1664306410},{"name":"Segment 3","startTime":1664306411,"endTime":1664306412},{"name":"Segment 4","startTime":1664306413,"endTime":1664306422},{"name":"Segment 5","startTime":1664306455,"endTime":1664306458},{"name":"Segment 6","startTime":1664306543,"endTime":1664306545},{"name":"Segment 7","startTime":1664306581,"endTime":1664306599},{"name":"Segment 8","startTime":1664306956,"endTime":1664306959},{"name":"Segment 9","startTime":1664306962,"endTime":1664306965},{"name":"Segment 10","startTime":1664307015,"endTime":1664307018},{"name":"Segment 11","startTime":1664307036,"endTime":1664307039},{"name":"Segment 12","startTime":1664307055,"endTime":1664307149},{"name":"Segment 13","startTime":1664307152,"endTime":1664307159},{"name":"Segment 14","startTime":1664307169,"endTime":1664307198},{"name":"Segment 15","startTime":1664307254,"endTime":1664307270},{"name":"Segment 16","startTime":1664307272,"endTime":1664307279},{"name":"Working on stuff","startTime":1664307284,"endTime":1664307290},{"name":"Segment 18","startTime":1664307593,"endTime":1664307611},{"name":"Segment 19","startTime":1664307842,"endTime":1664307851}]} ```