Compare commits

...

29 commits

Author SHA1 Message Date
Ell
7f75b08e4d 0.2.2 2024-05-19 10:31:50 +02:00
Ell
ebecd57b91 added discord link to settings 2024-05-19 10:29:55 +02:00
Ell
3186f1ba4d Merge remote-tracking branch 'origin/master' 2024-05-08 17:25:50 +02:00
Ell
ea04e198d9 add discord to readme 2024-05-08 17:25:42 +02:00
Jacobtread
bf9ac5636c
Feature prompt before delete (#48)
* fix: file rename bug

* feat: prompt user before deleting
2024-05-01 10:40:26 +02:00
Ell
6823de48a0 get rid of the roadmap section in the README 2024-04-15 11:31:26 +02:00
Jacobtread
70cb1594ef
fix: file rename bug (#46) 2024-04-15 11:24:15 +02:00
Ell
66eb45d253 0.2.1 2024-03-27 14:26:24 +01:00
Ell
4b6abebe61 still allow editing name and start time while running
closes #41
2024-03-27 14:24:57 +01:00
Ell
4c88c3282d finished up segment name markdown rendering 2024-03-27 14:21:45 +01:00
artel-ksikora
739e51326f
feat: add tag display to Segment (#43)
* feat: add tag display to Segment

* feat: Fragment markdown rework

* feat: Fragment markdown rework

* fixL prettifer remove

* fix: remove prettifer ignore
2024-03-18 16:11:11 +01:00
Ell
48d576aaa2 0.2.0 2024-02-26 15:37:54 +01:00
Ell
701be28601 use the hover background color 2024-02-26 15:36:28 +01:00
Ell
1d90a17e69 added timestamp display for durations
closes #20
2024-02-26 15:21:24 +01:00
Ell
7633ac8144 hover highlighting for the table 2024-02-26 14:56:31 +01:00
Ell
7e108a85cb right-align the buttons 2024-02-26 14:53:33 +01:00
Ell
5bd9fefe60 only indent the names, not the values 2024-02-26 14:41:25 +01:00
Ell
3469232d3b fixed some issues with how and when entry content is displayed
closes #36
2024-02-26 14:39:48 +01:00
Ell
92b2a287aa allow displaying segments in reverse
closes #39
2024-02-26 14:24:21 +01:00
Ell
51d1183c0f 0.1.8 2023-09-08 14:06:12 +02:00
Ell
17471e2831 fixed (newly introduced) exception when trying to edit a running timer 2023-09-08 14:02:15 +02:00
Ell
e95f62fb9d store timestamps as iso instead of unix
closes #26
2023-09-08 13:59:50 +02:00
Ell
f766fa7210 merge 2023-09-08 13:26:35 +02:00
Ell
3be8d8de02 ignore vault files 2023-08-11 19:32:43 +02:00
Matt Wiseley
a6727bb879 Handle active timer with no end value 2023-07-11 22:17:42 -04:00
Matt Wiseley
3d9e6163e7 Use fixed format for editable timestamps 2023-07-05 09:57:58 -04:00
Matt Wiseley
0612a4bd21 24 hour default time stamp format 2023-07-04 17:51:54 -04:00
Matt Wiseley
5677e02243 Remove unneeded width setting 2023-07-03 12:58:04 -04:00
Matt Wiseley
94631bc293 Editable timestamps and mobile horizontal scroll 2023-07-03 12:23:19 -04:00
24 changed files with 345 additions and 1089 deletions

View file

@ -1,9 +1,9 @@
# top-most EditorConfig file # top-most EditorConfig file
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
tab_width = 4 tab_width = 4

49
.gitignore vendored
View file

@ -1,23 +1,26 @@
# vscode # vscode
.vscode .vscode
# Intellij # Intellij
*.iml *.iml
.idea .idea
# npm # npm
node_modules node_modules
# Don't include the compiled main.js file in the repo. # Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead. # They should be uploaded to GitHub releases instead.
/main.js /main.js
# Exclude sourcemaps # Exclude local settings
*.map data.json
# obsidian # Exclude sourcemaps
workspace *.map
workspace.json
# obsidian
# Exclude macOS Finder (System Explorer) View States workspace
.DS_Store workspace.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

View file

@ -8,16 +8,16 @@ To get started tracking your time with Super Simple Time Tracker, open up the no
When switching to live preview or reading mode, you will now see the time tracker you just inserted! Now, simply name the first segment (or leave the box empty if you don't want to name it) and press the **Start** button. Once you're done with the thing you were doing, simply press the **End** button and the time you spent will be saved and displayed to you in the table. When switching to live preview or reading mode, you will now see the time tracker you just inserted! Now, simply name the first segment (or leave the box empty if you don't want to name it) and press the **Start** button. Once you're done with the thing you were doing, simply press the **End** button and the time you spent will be saved and displayed to you in the table.
Need help using the plugin? Feel free to join the Discord server!
[![Join the Discord server](https://ellpeck.de/res/discord-wide.png)](https://link.ellpeck.de/discordweb)
# 👀 What it does # 👀 What it does
A time tracker is really just a special code block that stores information about the times you pressed the Start and End buttons on. Since time is tracked solely through timestamps, you can switch notes, close Obsidian or even shut down your device completely while the tracker is running! Once you come back, your time tracker will still be running. A time tracker is really just a special code block that stores information about the times you pressed the Start and End buttons on. Since time is tracked solely through timestamps, you can switch notes, close Obsidian or even shut down your device completely while the tracker is running! Once you come back, your time tracker will still be running.
The tracker's information is stored in the code block as JSON data. The names, start times and end times of each segment are stored. They're displayed neatly in the code block in preview or reading mode. The tracker's information is stored in the code block as JSON data. The names, start times and end times of each segment are stored. They're displayed neatly in the code block in preview or reading mode.
# 🛣️ Roadmap
Super Simple Time Tracker is still in its early stages! There are a lot of plans for it, including:
- A setting to link segments to corresponding daily notes automatically
# 🙏 Acknowledgements # 🙏 Acknowledgements
If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image! If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!
[![Support me (if you want), via Patreon, Ko-fi or GitHub Sponsors](https://ellpeck.de/res/generalsupport.png)](https://ellpeck.de/support) [![Support me (if you want), via Patreon, Ko-fi or GitHub Sponsors](https://ellpeck.de/res/generalsupport-wide.png)](https://ellpeck.de/support)

View file

@ -1,7 +1,7 @@
{ {
"id": "simple-time-tracker", "id": "simple-time-tracker",
"name": "Super Simple Time Tracker", "name": "Super Simple Time Tracker",
"version": "0.1.7", "version": "0.2.2",
"minAppVersion": "1.2.8", "minAppVersion": "1.2.8",
"description": "Multi-purpose time trackers for your notes!", "description": "Multi-purpose time trackers for your notes!",
"author": "Ellpeck", "author": "Ellpeck",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "simple-time-tracker", "name": "simple-time-tracker",
"version": "0.1.6", "version": "0.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "simple-time-tracker", "name": "simple-time-tracker",
"version": "0.1.6", "version": "0.2.2",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{ {
"name": "simple-time-tracker", "name": "simple-time-tracker",
"version": "0.1.7", "version": "0.2.2",
"description": "Multi-purpose time trackers for your notes!", "description": "Multi-purpose time trackers for your notes!",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
import { Plugin } from "obsidian"; import {MarkdownRenderChild, Plugin, TFile} from "obsidian";
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings"; import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
import { SimpleTimeTrackerSettingsTab } from "./settings-tab"; import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
import { displayTracker, loadTracker, Tracker } from "./tracker"; import { displayTracker, loadTracker } from "./tracker";
export default class SimpleTimeTrackerPlugin extends Plugin { export default class SimpleTimeTrackerPlugin extends Plugin {
@ -13,9 +13,28 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this)); this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => { this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
let tracker: Tracker = loadTracker(s);
e.empty(); e.empty();
displayTracker(tracker, e, i.sourcePath, () => i.getSectionInfo(e), this.settings); let component = new MarkdownRenderChild(e)
let tracker = loadTracker(s);
// Initial file name
let filePath = i.sourcePath;
// Getter passed to displayTracker since the file name can change
const getFile = () => filePath;
// Hook rename events to update the file path
const renameEventRef = this.app.vault.on("rename", (file, oldPath) => {
if (file instanceof TFile && oldPath === filePath) {
filePath = file.path;
}
})
// Register the event to remove on unload
component.registerEvent(renameEventRef);
displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
i.addChild(component)
}); });
this.addCommand({ this.addCommand({

View file

@ -1,6 +1,6 @@
import { App, PluginSettingTab, Setting } from "obsidian"; import {App, PluginSettingTab, Setting} from "obsidian";
import SimpleTimeTrackerPlugin from "./main"; import SimpleTimeTrackerPlugin from "./main";
import { defaultSettings } from "./settings"; import {defaultSettings} from "./settings";
export class SimpleTimeTrackerSettingsTab extends PluginSettingTab { export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
@ -52,12 +52,38 @@ export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
}); });
}); });
this.containerEl.createEl("hr"); new Setting(this.containerEl)
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!"}); .setName("Timestamp Durations")
this.containerEl.createEl("a", {href: "https://ellpeck.de/support"}) .setDesc("Whether durations should be displayed in a timestamp format (12:15:01) rather than the default duration format (12h 15m 1s).")
.createEl("img", { .addToggle(t => {
attr: {src: "https://ellpeck.de/res/generalsupport.png"}, t.setValue(this.plugin.settings.timestampDurations);
cls: "simple-time-tracker-support" t.onChange(async v => {
this.plugin.settings.timestampDurations = v;
await this.plugin.saveSettings();
});
}); });
new 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(async v => {
this.plugin.settings.reverseSegmentOrder = v;
await this.plugin.saveSettings();
});
});
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: "simple-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"
});
} }
} }

View file

@ -1,13 +1,19 @@
export const defaultSettings: SimpleTimeTrackerSettings = { export const defaultSettings: SimpleTimeTrackerSettings = {
timestampFormat: "YY-MM-DD hh:mm:ss", timestampFormat: "YY-MM-DD HH:mm:ss",
editableTimestampFormat: "YYYY-MM-DD HH:mm:ss",
csvDelimiter: ",", csvDelimiter: ",",
fineGrainedDurations: true fineGrainedDurations: true,
reverseSegmentOrder: false,
timestampDurations: false
}; };
export interface SimpleTimeTrackerSettings { export interface SimpleTimeTrackerSettings {
timestampFormat: string; timestampFormat: string;
editableTimestampFormat: string;
csvDelimiter: string; csvDelimiter: string;
fineGrainedDurations: boolean; fineGrainedDurations: boolean;
reverseSegmentOrder: boolean;
timestampDurations: boolean;
} }

View file

@ -1,5 +1,5 @@
import { moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile } from "obsidian"; import {moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild} from "obsidian";
import { SimpleTimeTrackerSettings } from "./settings"; import {SimpleTimeTrackerSettings} from "./settings";
export interface Tracker { export interface Tracker {
entries: Entry[]; entries: Entry[];
@ -7,8 +7,8 @@ export interface Tracker {
export interface Entry { export interface Entry {
name: string; name: string;
startTime: number; startTime: string;
endTime: number; endTime: string;
subEntries: Entry[]; subEntries: Entry[];
} }
@ -31,7 +31,9 @@ export async function saveTracker(tracker: Tracker, app: App, fileName: string,
export function loadTracker(json: string): Tracker { export function loadTracker(json: string): Tracker {
if (json) { if (json) {
try { try {
return JSON.parse(json); let ret = JSON.parse(json);
fixLegacyTimestamps(ret.entries);
return ret;
} catch (e) { } catch (e) {
console.log(`Failed to parse Tracker from ${json}`); console.log(`Failed to parse Tracker from ${json}`);
} }
@ -39,7 +41,11 @@ export function loadTracker(json: string): Tracker {
return {entries: []}; return {entries: []};
} }
export function displayTracker(tracker: Tracker, element: HTMLElement, file: string, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings): void { type GetFile = () => string;
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
element.addClass("simple-time-tracker-container");
// add start/stop controls // add start/stop controls
let running = isRunning(tracker); let running = isRunning(tracker);
let btn = new ButtonComponent(element) let btn = new ButtonComponent(element)
@ -52,7 +58,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, file: str
} else { } else {
startNewEntry(tracker, newSegmentNameBox.getValue()); startNewEntry(tracker, newSegmentNameBox.getValue());
} }
await saveTracker(tracker, this.app, file, getSectionInfo()); await saveTracker(tracker, this.app, getFile(), getSectionInfo());
}); });
btn.buttonEl.addClass("simple-time-tracker-btn"); btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new TextComponent(element) let newSegmentNameBox = new TextComponent(element)
@ -79,8 +85,8 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, file: str
createEl("th", {text: "Duration"}), createEl("th", {text: "Duration"}),
createEl("th")); createEl("th"));
for (let entry of tracker.entries) for (let entry of orderedEntries(tracker.entries, settings))
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, 0); addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
// add copy buttons // add copy buttons
let buttons = element.createEl("div", {cls: "simple-time-tracker-bottom"}); let buttons = element.createEl("div", {cls: "simple-time-tracker-bottom"});
@ -104,7 +110,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, file: str
}, 1000); }, 1000);
} }
function startSubEntry(entry: Entry, name: string) { function startSubEntry(entry: Entry, name: string): void {
// if this entry is not split yet, we add its time as a sub-entry instead // if this entry is not split yet, we add its time as a sub-entry instead
if (!entry.subEntries) { if (!entry.subEntries) {
entry.subEntries = [{...entry, name: `Part 1`}]; entry.subEntries = [{...entry, name: `Part 1`}];
@ -114,19 +120,19 @@ function startSubEntry(entry: Entry, name: string) {
if (!name) if (!name)
name = `Part ${entry.subEntries.length + 1}`; name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({name: name, startTime: moment().unix(), endTime: null, subEntries: null}); entry.subEntries.push({name: name, startTime: moment().toISOString(), endTime: null, subEntries: null});
} }
function startNewEntry(tracker: Tracker, name: string): void { function startNewEntry(tracker: Tracker, name: string): void {
if (!name) if (!name)
name = `Segment ${tracker.entries.length + 1}`; name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = {name: name, startTime: moment().unix(), endTime: null, subEntries: null}; let entry: Entry = {name: name, startTime: moment().toISOString(), endTime: null, subEntries: null};
tracker.entries.push(entry); tracker.entries.push(entry);
} }
function endRunningEntry(tracker: Tracker): void { function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries); let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().unix(); entry.endTime = moment().toISOString();
} }
function removeEntry(entries: Entry[], toRemove: Entry): boolean { function removeEntry(entries: Entry[], toRemove: Entry): boolean {
@ -170,12 +176,12 @@ function getRunningEntry(entries: Entry[]): Entry {
return null; return null;
} }
function getDuration(entry: Entry) { function getDuration(entry: Entry): number {
if (entry.subEntries) { if (entry.subEntries) {
return getTotalDuration(entry.subEntries); return getTotalDuration(entry.subEntries);
} else { } else {
let endTime = entry.endTime ? moment.unix(entry.endTime) : moment(); let endTime = entry.endTime ? moment(entry.endTime) : moment();
return endTime.diff(moment.unix(entry.startTime)); return endTime.diff(moment(entry.startTime));
} }
} }
@ -186,7 +192,7 @@ function getTotalDuration(entries: Entry[]): number {
return ret; return ret;
} }
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings) { function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
let running = getRunningEntry(tracker.entries); let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) { if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running), settings)); current.setText(formatDuration(getDuration(running), settings));
@ -197,36 +203,64 @@ function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLE
total.setText(formatDuration(getTotalDuration(tracker.entries), settings)); total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
} }
function formatTimestamp(timestamp: number, settings: SimpleTimeTrackerSettings): string { function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment.unix(timestamp).format(settings.timestampFormat); return moment(timestamp).format(settings.timestampFormat);
}
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment(timestamp).format(settings.editableTimestampFormat);
}
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
return moment(formatted, settings.editableTimestampFormat).toISOString();
} }
function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string { function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
let ret = ""; let ret = "";
let duration = moment.duration(totalTime); let duration = moment.duration(totalTime);
let hours: number; let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
if (settings.fineGrainedDurations) {
if (duration.years() > 0) if (settings.timestampDurations) {
ret += duration.years() + "y "; if (settings.fineGrainedDurations) {
if (duration.months() > 0) let days = Math.floor(duration.asDays());
ret += duration.months() + "M "; if (days > 0)
if (duration.days() > 0) ret += days + ".";
ret += duration.days() + "d "; }
hours = duration.hours(); ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`;
} else { } else {
hours = Math.floor(duration.asHours()); 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";
} }
if (hours > 0)
ret += hours + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
return ret; return ret;
} }
function fixLegacyTimestamps(entries: Entry[]): void {
for (let entry of entries) {
if (entry.startTime && !isNaN(+entry.startTime))
entry.startTime = moment.unix(+entry.startTime).toISOString();
if (entry.endTime && !isNaN(+entry.endTime))
entry.endTime = moment.unix(+entry.endTime).toISOString();
if (entry.subEntries)
fixLegacyTimestamps(entry.subEntries);
}
}
function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let table = [["Segment", "Start time", "End time", "Duration"]]; let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of tracker.entries) for (let entry of orderedEntries(tracker.entries, settings))
table.push(...createTableSection(entry, settings)); table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]); table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
@ -248,7 +282,7 @@ function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettin
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string { function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = ""; let ret = "";
for (let entry of tracker.entries) { for (let entry of orderedEntries(tracker.entries, settings)) {
for (let row of createTableSection(entry, settings)) for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n"; ret += row.join(settings.csvDelimiter) + "\n";
} }
@ -256,60 +290,70 @@ function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): strin
} }
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] { function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
let ret: string[][] = [[ let ret = [[
entry.name, entry.name,
entry.startTime ? formatTimestamp(entry.startTime, settings) : "", entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
entry.endTime ? formatTimestamp(entry.endTime, settings) : "", entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]]; entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]];
if (entry.subEntries) { if (entry.subEntries) {
for (let sub of entry.subEntries) for (let sub of orderedEntries(entry.subEntries, settings))
ret.push(...createTableSection(sub, settings)); ret.push(...createTableSection(sub, settings));
} }
return ret; return ret;
} }
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, running: boolean, file: string, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number) { function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
}
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
let entryRunning = getRunningEntry(tracker.entries) == entry;
let row = table.createEl("tr"); let row = table.createEl("tr");
let name = row.createEl("td"); let nameField = new EditableField(row, indent, entry.name);
let namePar = name.createEl("span", {text: entry.name}); let startField = new EditableTimestampField(row, (entry.startTime), settings);
namePar.style.marginLeft = `${indent}em`; let endField = new EditableTimestampField(row, (entry.endTime), settings);
let nameBox = new TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", {text: entry.startTime ? formatTimestamp(entry.startTime, settings) : ""});
row.createEl("td", {text: entry.endTime ? formatTimestamp(entry.endTime, settings) : ""});
row.createEl("td", {text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""}); row.createEl("td", {text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""});
renderNameAsMarkdown(nameField.label, getFile, component);
let entryButtons = row.createEl("td"); let entryButtons = row.createEl("td");
if (!running) { entryButtons.addClass("simple-time-tracker-table-buttons");
new ButtonComponent(entryButtons) new ButtonComponent(entryButtons)
.setClass("clickable-icon") .setClass("clickable-icon")
.setIcon(`lucide-play`) .setIcon(`lucide-play`)
.setTooltip("Continue") .setTooltip("Continue")
.onClick(async () => { .setDisabled(trackerRunning)
startSubEntry(entry, newSegmentNameBox.getValue()); .onClick(async () => {
await saveTracker(tracker, this.app, file, getSectionInfo()); startSubEntry(entry, newSegmentNameBox.getValue());
}); await saveTracker(tracker, this.app, getFile(), getSectionInfo());
} });
let editButton = new ButtonComponent(entryButtons) let editButton = new ButtonComponent(entryButtons)
.setClass("clickable-icon") .setClass("clickable-icon")
.setTooltip("Edit") .setTooltip("Edit")
.setIcon("lucide-pencil") .setIcon("lucide-pencil")
.onClick(async () => { .onClick(async () => {
if (namePar.hidden) { if (nameField.editing()) {
namePar.hidden = false; entry.name = nameField.endEdit();
nameBox.inputEl.hidden = true; startField.endEdit();
editButton.setIcon("lucide-pencil"); entry.startTime = startField.getTimestamp();
if (nameBox.getValue()) { if (!entryRunning) {
entry.name = nameBox.getValue(); endField.endEdit();
namePar.setText(entry.name); entry.endTime = endField.getTimestamp();
await saveTracker(tracker, this.app, file, getSectionInfo());
} }
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
editButton.setIcon("lucide-pencil");
renderNameAsMarkdown(nameField.label, getFile, component);
} else { } else {
namePar.hidden = true; nameField.beginEdit(entry.name);
nameBox.inputEl.hidden = false; // only allow editing start and end times if we don't have sub entries
nameBox.setValue(entry.name); if (!entry.subEntries) {
startField.beginEdit(entry.startTime);
if (!entryRunning)
endField.beginEdit(entry.endTime);
}
editButton.setIcon("lucide-check"); editButton.setIcon("lucide-check");
} }
}); });
@ -317,13 +361,91 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
.setClass("clickable-icon") .setClass("clickable-icon")
.setTooltip("Remove") .setTooltip("Remove")
.setIcon("lucide-trash") .setIcon("lucide-trash")
.setDisabled(entryRunning)
.onClick(async () => { .onClick(async () => {
if (!confirm("Are you sure you want to delete this entry?")) {
return;
}
removeEntry(tracker.entries, entry); removeEntry(tracker.entries, entry);
await saveTracker(tracker, this.app, file, getSectionInfo()); await saveTracker(tracker, this.app, getFile(), getSectionInfo());
}); });
if (entry.subEntries) { if (entry.subEntries) {
for (let sub of entry.subEntries) for (let sub of orderedEntries(entry.subEntries, settings))
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent + 1); addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
}
}
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
// rendering wraps it in a paragraph
label.innerHTML = label.querySelector("p").innerHTML;
}
class EditableField {
cell: HTMLTableCellElement;
label: HTMLSpanElement;
box: TextComponent;
constructor(row: HTMLTableRowElement, indent: number, value: string) {
this.cell = row.createEl("td");
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.hide();
}
editing(): boolean {
return this.label.hidden;
}
beginEdit(value: string): void {
this.label.hidden = true;
this.box.setValue(value);
this.box.inputEl.show();
}
endEdit(): string {
const value = this.box.getValue();
this.label.setText(value);
this.box.inputEl.hide();
this.label.hidden = false;
return value;
}
}
class EditableTimestampField extends EditableField {
settings: SimpleTimeTrackerSettings;
constructor(row: HTMLTableRowElement, value: string, settings: SimpleTimeTrackerSettings) {
super(row, 0, value ? formatTimestamp(value, settings) : "");
this.settings = settings;
}
beginEdit(value: string): void {
super.beginEdit(value ? formatEditableTimestamp(value, this.settings) : "");
}
endEdit(): string {
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(): string {
if (this.box.getValue()) {
return unformatEditableTimestamp(this.box.getValue(), this.settings);
} else {
return null;
}
} }
} }

View file

@ -1,6 +1,9 @@
.simple-time-tracker-support { .simple-time-tracker-container {
max-width: 50%; overflow-x: scroll;
width: 400px; }
.simple-time-tracker-settings-image {
width: 100%;
height: auto; height: auto;
} }
@ -56,9 +59,23 @@
.simple-time-tracker-table td, .simple-time-tracker-table td,
.simple-time-tracker-table th { .simple-time-tracker-table th {
vertical-align: middle;
border: none; border: none;
} }
.simple-time-tracker-table .clickable-icon { .simple-time-tracker-table .clickable-icon {
display: inline; display: inline;
} }
.simple-time-tracker-input {
max-width: 150px;
min-width: 100px;
}
.simple-time-tracker-table-buttons {
text-align: right !important;
}
.simple-time-tracker-table tr:hover {
background-color: var(--background-modifier-hover);
}

View file

@ -25,5 +25,6 @@
"publish": false, "publish": false,
"sync": false, "sync": false,
"canvas": true, "canvas": true,
"bookmarks": true "bookmarks": true,
"properties": false
} }

View file

@ -0,0 +1,3 @@
*
!.gitignore
!data.json

View file

@ -1,5 +1,8 @@
{ {
"timestampFormat": "YY-MM-DD hh:mm:ss", "timestampFormat": "YY-MM-DD hh:mm:ss",
"editableTimestampFormat": "YYYY-MM-DD HH:mm:ss",
"csvDelimiter": ",", "csvDelimiter": ",",
"fineGrainedDurations": false "fineGrainedDurations": true,
"reverseSegmentOrder": false,
"timestampDurations": true
} }

View file

@ -1,404 +0,0 @@
/*
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: () => SimpleTimeTrackerPlugin
});
var import_obsidian3 = __toModule(require("obsidian"));
// src/settings.ts
var defaultSettings = {
timestampFormat: "YY-MM-DD hh:mm:ss",
csvDelimiter: ",",
fineGrainedDurations: true
};
// src/settings-tab.ts
var import_obsidian = __toModule(require("obsidian"));
var SimpleTimeTrackerSettingsTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
this.containerEl.empty();
this.containerEl.createEl("h2", { text: "Super Simple 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();
}));
});
this.containerEl.createEl("hr");
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.png" },
cls: "simple-time-tracker-support"
});
}
};
// src/tracker.ts
var import_obsidian2 = __toModule(require("obsidian"));
function saveTracker(tracker, app, 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 {
return JSON.parse(json);
} catch (e) {
console.log(`Failed to parse Tracker from ${json}`);
}
}
return { entries: [] };
}
function displayTracker(tracker, element, file, getSectionInfo, settings) {
let running = isRunning(tracker);
let btn = new import_obsidian2.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, this.app, file, getSectionInfo());
}));
btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new import_obsidian2.TextComponent(element).setPlaceholder("Segment name").setDisabled(running);
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
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" });
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" });
totalDiv.createEl("span", { text: "Total" });
if (tracker.entries.length > 0) {
let table = element.createEl("table", { cls: "simple-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 tracker.entries)
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, 0);
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
new import_obsidian2.ButtonComponent(buttons).setButtonText("Copy as table").onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
new import_obsidian2.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 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_obsidian2.moment)().unix(), endTime: null, subEntries: null });
}
function startNewEntry(tracker, name) {
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry = { name, startTime: (0, import_obsidian2.moment)().unix(), endTime: null, subEntries: null };
tracker.entries.push(entry);
}
function endRunningEntry(tracker) {
let entry = getRunningEntry(tracker.entries);
entry.endTime = (0, import_obsidian2.moment)().unix();
}
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 = null;
}
return true;
}
}
}
return false;
}
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 getDuration(entry) {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
let endTime = entry.endTime ? import_obsidian2.moment.unix(entry.endTime) : (0, import_obsidian2.moment)();
return endTime.diff(import_obsidian2.moment.unix(entry.startTime));
}
}
function getTotalDuration(entries) {
let ret = 0;
for (let entry of entries)
ret += getDuration(entry);
return ret;
}
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 formatTimestamp(timestamp, settings) {
return import_obsidian2.moment.unix(timestamp).format(settings.timestampFormat);
}
function formatDuration(totalTime, settings) {
let ret = "";
let duration = import_obsidian2.moment.duration(totalTime);
let hours;
if (settings.fineGrainedDurations) {
if (duration.years() > 0)
ret += duration.years() + "y ";
if (duration.months() > 0)
ret += duration.months() + "M ";
if (duration.days() > 0)
ret += duration.days() + "d ";
hours = duration.hours();
} else {
hours = Math.floor(duration.asHours());
}
if (hours > 0)
ret += hours + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
return ret;
}
function createMarkdownTable(tracker, settings) {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of tracker.entries)
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), 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 tracker.entries) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
return ret;
}
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 entry.subEntries)
ret.push(...createTableSection(sub, settings));
}
return ret;
}
function addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent) {
let row = table.createEl("tr");
let name = row.createEl("td");
let namePar = name.createEl("span", { text: entry.name });
namePar.style.marginLeft = `${indent}em`;
let nameBox = new import_obsidian2.TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", { text: entry.startTime ? formatTimestamp(entry.startTime, settings) : "" });
row.createEl("td", { text: entry.endTime ? formatTimestamp(entry.endTime, settings) : "" });
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" });
let entryButtons = row.createEl("td");
if (!running) {
new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setIcon(`lucide-play`).setTooltip("Continue").onClick(() => __async(this, null, function* () {
startSubEntry(entry, newSegmentNameBox.getValue());
yield saveTracker(tracker, this.app, file, getSectionInfo());
}));
}
let editButton = new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Edit").setIcon("lucide-pencil").onClick(() => __async(this, null, function* () {
if (namePar.hidden) {
namePar.hidden = false;
nameBox.inputEl.hidden = true;
editButton.setIcon("lucide-pencil");
if (nameBox.getValue()) {
entry.name = nameBox.getValue();
namePar.setText(entry.name);
yield saveTracker(tracker, this.app, file, getSectionInfo());
}
} else {
namePar.hidden = true;
nameBox.inputEl.hidden = false;
nameBox.setValue(entry.name);
editButton.setIcon("lucide-check");
}
}));
new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Remove").setIcon("lucide-trash").onClick(() => __async(this, null, function* () {
removeEntry(tracker.entries, entry);
yield saveTracker(tracker, this.app, file, getSectionInfo());
}));
if (entry.subEntries) {
for (let sub of entry.subEntries)
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent + 1);
}
}
// src/main.ts
var SimpleTimeTrackerPlugin = class extends import_obsidian3.Plugin {
onload() {
return __async(this, null, function* () {
yield this.loadSettings();
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
let tracker = loadTracker(s);
e.empty();
displayTracker(tracker, e, i.sourcePath, () => i.getSectionInfo(e), this.settings);
});
this.addCommand({
id: `insert`,
name: `Insert Time Tracker`,
editorCallback: (e, _) => {
e.replaceSelection("```simple-time-tracker\n```\n");
}
});
});
}
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);
});
}
};

View file

@ -1,404 +0,0 @@
/*
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: () => SimpleTimeTrackerPlugin
});
var import_obsidian3 = __toModule(require("obsidian"));
// src/settings.ts
var defaultSettings = {
timestampFormat: "YY-MM-DD hh:mm:ss",
csvDelimiter: ",",
fineGrainedDurations: true
};
// src/settings-tab.ts
var import_obsidian = __toModule(require("obsidian"));
var SimpleTimeTrackerSettingsTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
this.containerEl.empty();
this.containerEl.createEl("h2", { text: "Super Simple 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();
}));
});
this.containerEl.createEl("hr");
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.png" },
cls: "simple-time-tracker-support"
});
}
};
// src/tracker.ts
var import_obsidian2 = __toModule(require("obsidian"));
function saveTracker(tracker, app, 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 {
return JSON.parse(json);
} catch (e) {
console.log(`Failed to parse Tracker from ${json}`);
}
}
return { entries: [] };
}
function displayTracker(tracker, element, file, getSectionInfo, settings) {
let running = isRunning(tracker);
let btn = new import_obsidian2.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, this.app, file, getSectionInfo());
}));
btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new import_obsidian2.TextComponent(element).setPlaceholder("Segment name").setDisabled(running);
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
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" });
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" });
totalDiv.createEl("span", { text: "Total" });
if (tracker.entries.length > 0) {
let table = element.createEl("table", { cls: "simple-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 tracker.entries)
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, 0);
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
new import_obsidian2.ButtonComponent(buttons).setButtonText("Copy as table").onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
new import_obsidian2.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 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_obsidian2.moment)().unix(), endTime: null, subEntries: null });
}
function startNewEntry(tracker, name) {
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry = { name, startTime: (0, import_obsidian2.moment)().unix(), endTime: null, subEntries: null };
tracker.entries.push(entry);
}
function endRunningEntry(tracker) {
let entry = getRunningEntry(tracker.entries);
entry.endTime = (0, import_obsidian2.moment)().unix();
}
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 = null;
}
return true;
}
}
}
return false;
}
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 getDuration(entry) {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
let endTime = entry.endTime ? import_obsidian2.moment.unix(entry.endTime) : (0, import_obsidian2.moment)();
return endTime.diff(import_obsidian2.moment.unix(entry.startTime));
}
}
function getTotalDuration(entries) {
let ret = 0;
for (let entry of entries)
ret += getDuration(entry);
return ret;
}
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 formatTimestamp(timestamp, settings) {
return import_obsidian2.moment.unix(timestamp).format(settings.timestampFormat);
}
function formatDuration(totalTime, settings) {
let ret = "";
let duration = import_obsidian2.moment.duration(totalTime);
let hours;
if (settings.fineGrainedDurations) {
if (duration.years() > 0)
ret += duration.years() + "y ";
if (duration.months() > 0)
ret += duration.months() + "M ";
if (duration.days() > 0)
ret += duration.days() + "d ";
hours = duration.hours();
} else {
hours = Math.floor(duration.asHours());
}
if (hours > 0)
ret += hours + "h ";
if (duration.minutes() > 0)
ret += duration.minutes() + "m ";
ret += duration.seconds() + "s";
return ret;
}
function createMarkdownTable(tracker, settings) {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of tracker.entries)
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), 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 tracker.entries) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
return ret;
}
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 entry.subEntries)
ret.push(...createTableSection(sub, settings));
}
return ret;
}
function addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent) {
let row = table.createEl("tr");
let name = row.createEl("td");
let namePar = name.createEl("span", { text: entry.name });
namePar.style.marginLeft = `${indent}em`;
let nameBox = new import_obsidian2.TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", { text: entry.startTime ? formatTimestamp(entry.startTime, settings) : "" });
row.createEl("td", { text: entry.endTime ? formatTimestamp(entry.endTime, settings) : "" });
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" });
let entryButtons = row.createEl("td");
if (!running) {
new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setIcon(`lucide-play`).setTooltip("Continue").onClick(() => __async(this, null, function* () {
startSubEntry(entry, newSegmentNameBox.getValue());
yield saveTracker(tracker, this.app, file, getSectionInfo());
}));
}
let editButton = new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Edit").setIcon("lucide-pencil").onClick(() => __async(this, null, function* () {
if (namePar.hidden) {
namePar.hidden = false;
nameBox.inputEl.hidden = true;
editButton.setIcon("lucide-pencil");
if (nameBox.getValue()) {
entry.name = nameBox.getValue();
namePar.setText(entry.name);
yield saveTracker(tracker, this.app, file, getSectionInfo());
}
} else {
namePar.hidden = true;
nameBox.inputEl.hidden = false;
nameBox.setValue(entry.name);
editButton.setIcon("lucide-check");
}
}));
new import_obsidian2.ButtonComponent(entryButtons).setClass("clickable-icon").setTooltip("Remove").setIcon("lucide-trash").onClick(() => __async(this, null, function* () {
removeEntry(tracker.entries, entry);
yield saveTracker(tracker, this.app, file, getSectionInfo());
}));
if (entry.subEntries) {
for (let sub of entry.subEntries)
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent + 1);
}
}
// src/main.ts
var SimpleTimeTrackerPlugin = class extends import_obsidian3.Plugin {
onload() {
return __async(this, null, function* () {
yield this.loadSettings();
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
let tracker = loadTracker(s);
e.empty();
displayTracker(tracker, e, i.sourcePath, () => i.getSectionInfo(e), this.settings);
});
this.addCommand({
id: `insert`,
name: `Insert Time Tracker`,
editorCallback: (e, _) => {
e.replaceSelection("```simple-time-tracker\n```\n");
}
});
});
}
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);
});
}
};

View file

@ -1,10 +0,0 @@
{
"id": "simple-time-tracker",
"name": "Super Simple Time Tracker",
"version": "0.1.7",
"minAppVersion": "1.2.8",
"description": "Multi-purpose time trackers for your notes!",
"author": "Ellpeck",
"authorUrl": "https://ellpeck.de",
"isDesktopOnly": false
}

View file

@ -1,10 +0,0 @@
{
"id": "simple-time-tracker",
"name": "Super Simple Time Tracker",
"version": "0.1.7",
"minAppVersion": "1.2.8",
"description": "Multi-purpose time trackers for your notes!",
"author": "Ellpeck",
"authorUrl": "https://ellpeck.de",
"isDesktopOnly": false
}

View file

@ -1,64 +0,0 @@
.simple-time-tracker-support {
max-width: 50%;
width: 400px;
height: auto;
}
.simple-time-tracker-btn,
.simple-time-tracker-txt {
display: block;
margin-left: auto;
margin-right: auto;
}
.simple-time-tracker-txt {
text-align: center;
}
.simple-time-tracker-btn {
margin-top: 10px;
margin-bottom: 10px;
}
.simple-time-tracker-btn svg {
width: 32px;
height: 32px;
}
.simple-time-tracker-bottom button {
margin: 10px 5px 10px 5px;
}
.simple-time-tracker-timers,
.simple-time-tracker-bottom {
display: flex;
justify-content: center;
text-align: center;
}
.simple-time-tracker-timers span {
display: block;
}
.simple-time-tracker-timer {
margin: 20px;
}
.simple-time-tracker-timer-time {
font-size: xx-large;
font-weight: bolder;
}
.simple-time-tracker-table {
width: 100%;
margin-top: 20px;
}
.simple-time-tracker-table td,
.simple-time-tracker-table th {
border: none;
}
.simple-time-tracker-table .clickable-icon {
display: inline;
}

View file

@ -1,64 +0,0 @@
.simple-time-tracker-support {
max-width: 50%;
width: 400px;
height: auto;
}
.simple-time-tracker-btn,
.simple-time-tracker-txt {
display: block;
margin-left: auto;
margin-right: auto;
}
.simple-time-tracker-txt {
text-align: center;
}
.simple-time-tracker-btn {
margin-top: 10px;
margin-bottom: 10px;
}
.simple-time-tracker-btn svg {
width: 32px;
height: 32px;
}
.simple-time-tracker-bottom button {
margin: 10px 5px 10px 5px;
}
.simple-time-tracker-timers,
.simple-time-tracker-bottom {
display: flex;
justify-content: center;
text-align: center;
}
.simple-time-tracker-timers span {
display: block;
}
.simple-time-tracker-timer {
margin: 20px;
}
.simple-time-tracker-timer-time {
font-size: xx-large;
font-weight: bolder;
}
.simple-time-tracker-table {
width: 100%;
margin-top: 20px;
}
.simple-time-tracker-table td,
.simple-time-tracker-table th {
border: none;
}
.simple-time-tracker-table .clickable-icon {
display: inline;
}

View file

@ -2,5 +2,9 @@
More notes for my cool project! This note shows that we can correctly display accumulated time that lasts longer than a 24 hour day! 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 ```simple-time-tracker
{"entries":[{"name":"test","startTime":1596265200,"endTime":1627887600,"subEntries":null},{"name":"test","startTime":1627801200,"endTime":1633158000,"subEntries":null},{"name":"test","startTime":1659337200,"endTime":1659423600,"subEntries":null},{"name":"test","startTime":1664627410,"endTime":1664631605,"subEntries":null},{"name":"Segment 5","startTime":1684858616,"endTime":1684858619,"subEntries":null}]} {"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
{"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}]}]}
``` ```

View file

@ -0,0 +1,4 @@
Tested for #tag, *italic*, [link](test2), etc:
```simple-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","subEntries":null},{"name":"Part 3","startTime":"2024-03-17T11:17:08.000Z","endTime":"2024-03-17T11:17:24.000Z","subEntries":null}]},{"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","subEntries":null},{"name":"#tag5 Part 2 *italic*","startTime":"2024-03-17T12:22:20.000Z","endTime":"2024-03-17T12:22:24.000Z","subEntries":null}]},{"name":"*italic* Segment 5 #tag6 [test2](test2)","startTime":"2024-03-17T12:40:37.000Z","endTime":"2024-03-17T12:40:45.000Z","subEntries":null},{"name":"Segment 6","startTime":"2024-03-27T13:20:56.000Z","endTime":null,"subEntries":null}]}
```

View file

@ -2,5 +2,5 @@
These are the notes for my cool project. There's so much left to do! I wish I had a way to track the amount of time I spend on each part of the project. These are the notes for my cool project. There's so much left to do! I wish I had a way to track the amount of time I spend on each part of the project.
```simple-time-tracker ```simple-time-tracker
{"entries":[{"name":"Segment 1","startTime":1666189948,"endTime":1666189951,"subEntries":null},{"name":"Segment 2","startTime":1666189953,"endTime":1666189961,"subEntries":null},{"name":"Segment 3","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":1666189962,"endTime":1666189995,"subEntries":null},{"name":"Part 2","startTime":1666190004,"endTime":1666190025,"subEntries":null},{"name":"Part 3","startTime":1666190094,"endTime":1666190101,"subEntries":null}]},{"name":"Segment 4","startTime":1666190088,"endTime":1666190091,"subEntries":null},{"name":"Segment 5","startTime":1684857704,"endTime":1684857708,"subEntries":null},{"name":"Segment 6","startTime":1684857710,"endTime":1684857712,"subEntries":null},{"name":"Segment 7","startTime":1684857729,"endTime":1684857732,"subEntries":null},{"name":"Segment 8","startTime":1684857743,"endTime":1684857748,"subEntries":null}]} {"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}]}
``` ```

View file

@ -7,5 +7,9 @@
"0.1.4": "0.15.0", "0.1.4": "0.15.0",
"0.1.5": "0.15.0", "0.1.5": "0.15.0",
"0.1.6": "0.15.0", "0.1.6": "0.15.0",
"0.1.7": "1.2.8" "0.1.7": "1.2.8",
"0.1.8": "1.3.0",
"0.2.0": "1.3.0",
"0.2.1": "1.3.0",
"0.2.2": "1.3.0"
} }