mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-06-29 01:18:19 +02:00
Compare commits
43 commits
Author | SHA1 | Date | |
---|---|---|---|
7f75b08e4d | |||
ebecd57b91 | |||
3186f1ba4d | |||
ea04e198d9 | |||
|
bf9ac5636c | ||
6823de48a0 | |||
|
70cb1594ef | ||
66eb45d253 | |||
4b6abebe61 | |||
4c88c3282d | |||
|
739e51326f | ||
48d576aaa2 | |||
701be28601 | |||
1d90a17e69 | |||
7633ac8144 | |||
7e108a85cb | |||
5bd9fefe60 | |||
3469232d3b | |||
92b2a287aa | |||
51d1183c0f | |||
17471e2831 | |||
e95f62fb9d | |||
f766fa7210 | |||
3be8d8de02 | |||
|
a6727bb879 | ||
|
3d9e6163e7 | ||
|
0612a4bd21 | ||
|
5677e02243 | ||
|
94631bc293 | ||
941c5e261b | |||
b5e768d99c | |||
175ef1e0d7 | |||
|
5baf96571e | ||
7aa91901c6 | |||
70ade8ada0 | |||
05a4710b8a | |||
e7815334b2 | |||
|
967d331172 | ||
|
85ee96f5dd | ||
402b97c799 | |||
1bda2accf4 | |||
132e2088be | |||
bce221aaaa |
|
@ -4,6 +4,6 @@ root = true
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = tab
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
tab_width = 4
|
tab_width = 4
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,6 +12,9 @@ node_modules
|
||||||
# They should be uploaded to GitHub releases instead.
|
# They should be uploaded to GitHub releases instead.
|
||||||
/main.js
|
/main.js
|
||||||
|
|
||||||
|
# Exclude local settings
|
||||||
|
data.json
|
||||||
|
|
||||||
# Exclude sourcemaps
|
# Exclude sourcemaps
|
||||||
*.map
|
*.map
|
||||||
|
|
||||||
|
|
10
README.md
10
README.md
|
@ -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)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"id": "simple-time-tracker",
|
"id": "simple-time-tracker",
|
||||||
"name": "Super Simple Time Tracker",
|
"name": "Super Simple Time Tracker",
|
||||||
"version": "0.1.3",
|
"version": "0.2.2",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "1.2.8",
|
||||||
"description": "Multi-purpose time trackers for your notes!",
|
"description": "Multi-purpose time trackers for your notes!",
|
||||||
"author": "Ellpeck",
|
"author": "Ellpeck",
|
||||||
"authorUrl": "https://ellpeck.de",
|
"authorUrl": "https://ellpeck.de",
|
||||||
|
|
1171
package-lock.json
generated
1171
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "simple-time-tracker",
|
"name": "simple-time-tracker",
|
||||||
"version": "0.1.3",
|
"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": {
|
||||||
|
|
29
src/main.ts
29
src/main.ts
|
@ -1,4 +1,4 @@
|
||||||
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 } from "./tracker";
|
import { displayTracker, loadTracker } from "./tracker";
|
||||||
|
@ -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 = loadTracker(s);
|
|
||||||
e.empty();
|
e.empty();
|
||||||
displayTracker(tracker, e, () => 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({
|
||||||
|
@ -27,11 +46,11 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings(): Promise<void> {
|
||||||
this.settings = Object.assign({}, defaultSettings, await this.loadData());
|
this.settings = Object.assign({}, defaultSettings, await this.loadData());
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings(): Promise<void> {
|
||||||
await this.saveData(this.settings);
|
await this.saveData(this.settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,9 +41,49 @@ export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new 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(async v => {
|
||||||
|
this.plugin.settings.fineGrainedDurations = v;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new 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(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("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("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" })
|
this.containerEl.createEl("a", { href: "https://ellpeck.de/support" }).createEl("img", {
|
||||||
.createEl("img", { attr: { src: "https://ellpeck.de/res/generalsupport.png" }, cls: "simple-time-tracker-support" });
|
attr: { src: "https://ellpeck.de/res/generalsupport-wide.png" },
|
||||||
|
cls: "simple-time-tracker-settings-image"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
export const defaultSettings: SimpleTimeTrackerSettings = {
|
export const defaultSettings: SimpleTimeTrackerSettings = {
|
||||||
timestampFormat: "YY-MM-DD hh:mm:ss",
|
timestampFormat: "YY-MM-DD HH:mm:ss",
|
||||||
csvDelimiter: ","
|
editableTimestampFormat: "YYYY-MM-DD HH:mm:ss",
|
||||||
|
csvDelimiter: ",",
|
||||||
|
fineGrainedDurations: true,
|
||||||
|
reverseSegmentOrder: false,
|
||||||
|
timestampDurations: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SimpleTimeTrackerSettings {
|
export interface SimpleTimeTrackerSettings {
|
||||||
|
|
||||||
timestampFormat: string;
|
timestampFormat: string;
|
||||||
|
editableTimestampFormat: string;
|
||||||
csvDelimiter: string;
|
csvDelimiter: string;
|
||||||
|
fineGrainedDurations: boolean;
|
||||||
|
reverseSegmentOrder: boolean;
|
||||||
|
timestampDurations: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
427
src/tracker.ts
427
src/tracker.ts
|
@ -1,4 +1,4 @@
|
||||||
import { moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent } 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 {
|
||||||
|
@ -7,29 +7,13 @@ export interface Tracker {
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
name: string;
|
name: string;
|
||||||
startTime: number;
|
startTime: string;
|
||||||
endTime: number;
|
endTime: string;
|
||||||
|
subEntries: Entry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startEntry(tracker: Tracker, name: string): void {
|
export async function saveTracker(tracker: Tracker, app: App, fileName: string, section: MarkdownSectionInformation): Promise<void> {
|
||||||
if (!name)
|
let file = app.vault.getAbstractFileByPath(fileName) as TFile;
|
||||||
name = `Segment ${tracker.entries.length + 1}`;
|
|
||||||
let entry: Entry = { name: name, startTime: moment().unix(), endTime: null };
|
|
||||||
tracker.entries.push(entry);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function endEntry(tracker: Tracker): void {
|
|
||||||
let last = tracker.entries.last();
|
|
||||||
last.endTime = moment().unix();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRunning(tracker: Tracker): boolean {
|
|
||||||
let last = tracker.entries.last();
|
|
||||||
return last != null && !last.endTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveTracker(tracker: Tracker, app: App, section: MarkdownSectionInformation): Promise<void> {
|
|
||||||
let file = app.workspace.getActiveFile();
|
|
||||||
if (!file)
|
if (!file)
|
||||||
return;
|
return;
|
||||||
let content = await app.vault.read(file);
|
let content = await app.vault.read(file);
|
||||||
|
@ -47,7 +31,9 @@ export async function saveTracker(tracker: Tracker, app: App, section: MarkdownS
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +41,11 @@ export function loadTracker(json: string): Tracker {
|
||||||
return {entries: []};
|
return {entries: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayTracker(tracker: Tracker, element: HTMLElement, 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)
|
||||||
|
@ -64,17 +54,17 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
|
||||||
.setTooltip(running ? "End" : "Start")
|
.setTooltip(running ? "End" : "Start")
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (running) {
|
if (running) {
|
||||||
endEntry(tracker);
|
endRunningEntry(tracker);
|
||||||
} else {
|
} else {
|
||||||
startEntry(tracker, name.getValue());
|
startNewEntry(tracker, newSegmentNameBox.getValue());
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, this.app, getSectionInfo());
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
});
|
});
|
||||||
btn.buttonEl.addClass("simple-time-tracker-btn");
|
btn.buttonEl.addClass("simple-time-tracker-btn");
|
||||||
let name = new TextComponent(element)
|
let newSegmentNameBox = new TextComponent(element)
|
||||||
.setPlaceholder("Segment name")
|
.setPlaceholder("Segment name")
|
||||||
.setDisabled(running);
|
.setDisabled(running);
|
||||||
name.inputEl.addClass("simple-time-tracker-txt");
|
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
|
||||||
|
|
||||||
// add timers
|
// add timers
|
||||||
let timer = element.createDiv({cls: "simple-time-tracker-timers"});
|
let timer = element.createDiv({cls: "simple-time-tracker-timers"});
|
||||||
|
@ -95,46 +85,8 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
|
||||||
createEl("th", {text: "Duration"}),
|
createEl("th", {text: "Duration"}),
|
||||||
createEl("th"));
|
createEl("th"));
|
||||||
|
|
||||||
for (let entry of tracker.entries) {
|
for (let entry of orderedEntries(tracker.entries, settings))
|
||||||
let row = table.createEl("tr");
|
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
|
||||||
|
|
||||||
let name = row.createEl("td");
|
|
||||||
let namePar = name.createEl("span", { text: entry.name });
|
|
||||||
let nameBox = new TextComponent(name).setValue(entry.name);
|
|
||||||
nameBox.inputEl.hidden = true;
|
|
||||||
|
|
||||||
row.createEl("td", { text: formatTimestamp(entry.startTime, settings) });
|
|
||||||
row.createEl("td", { text: entry.endTime ? formatTimestamp(entry.endTime, settings) : "" });
|
|
||||||
row.createEl("td", { text: entry.endTime ? formatDurationBetween(entry.startTime, entry.endTime) : "" });
|
|
||||||
|
|
||||||
let entryButtons = row.createEl("td");
|
|
||||||
let editButton = new ButtonComponent(entryButtons)
|
|
||||||
.setClass("clickable-icon")
|
|
||||||
.setTooltip("Edit")
|
|
||||||
.setIcon("lucide-pencil")
|
|
||||||
.onClick(() => {
|
|
||||||
if (namePar.hidden) {
|
|
||||||
namePar.hidden = false;
|
|
||||||
nameBox.inputEl.hidden = true;
|
|
||||||
if (nameBox.getValue())
|
|
||||||
namePar.setText(nameBox.getValue());
|
|
||||||
editButton.setIcon("lucide-pencil");
|
|
||||||
} else {
|
|
||||||
namePar.hidden = true;
|
|
||||||
nameBox.inputEl.hidden = false;
|
|
||||||
nameBox.setValue(namePar.getText());
|
|
||||||
editButton.setIcon("lucide-check");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
new ButtonComponent(entryButtons)
|
|
||||||
.setClass("clickable-icon")
|
|
||||||
.setTooltip("Remove")
|
|
||||||
.setIcon("lucide-trash")
|
|
||||||
.onClick(async () => {
|
|
||||||
tracker.entries.remove(entry);
|
|
||||||
await saveTracker(tracker, this.app, getSectionInfo());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"});
|
||||||
|
@ -147,60 +99,170 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getSectio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setCountdownValues(tracker, current, total, currentDiv);
|
setCountdownValues(tracker, current, total, currentDiv, settings);
|
||||||
let intervalId = window.setInterval(() => {
|
let intervalId = window.setInterval(() => {
|
||||||
// we delete the interval timer when the element is removed
|
// we delete the interval timer when the element is removed
|
||||||
if (!element.isConnected) {
|
if (!element.isConnected) {
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCountdownValues(tracker, current, total, currentDiv);
|
setCountdownValues(tracker, current, total, currentDiv, settings);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement) {
|
function startSubEntry(entry: Entry, name: string): void {
|
||||||
let currEntry = tracker.entries.last();
|
// if this entry is not split yet, we add its time as a sub-entry instead
|
||||||
if (currEntry) {
|
if (!entry.subEntries) {
|
||||||
if (!currEntry.endTime)
|
entry.subEntries = [{...entry, name: `Part 1`}];
|
||||||
current.setText(formatDurationBetween(currEntry.startTime, moment().unix()));
|
entry.startTime = null;
|
||||||
total.setText(formatDuration(getTotalDuration(tracker)));
|
entry.endTime = null;
|
||||||
}
|
|
||||||
currentDiv.hidden = !currEntry || !!currEntry.endTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalDuration(tracker: Tracker): number {
|
if (!name)
|
||||||
let totalDuration = 0;
|
name = `Part ${entry.subEntries.length + 1}`;
|
||||||
for (let entry of tracker.entries) {
|
entry.subEntries.push({name: name, startTime: moment().toISOString(), endTime: null, subEntries: null});
|
||||||
let endTime = entry.endTime ? moment.unix(entry.endTime) : moment();
|
|
||||||
totalDuration += endTime.diff(moment.unix(entry.startTime));
|
|
||||||
}
|
|
||||||
return totalDuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimestamp(timestamp: number, settings: SimpleTimeTrackerSettings): string {
|
function startNewEntry(tracker: Tracker, name: string): void {
|
||||||
return moment.unix(timestamp).format(settings.timestampFormat);
|
if (!name)
|
||||||
|
name = `Segment ${tracker.entries.length + 1}`;
|
||||||
|
let entry: Entry = {name: name, startTime: moment().toISOString(), endTime: null, subEntries: null};
|
||||||
|
tracker.entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDurationBetween(startTime: number, endTime: number): string {
|
function endRunningEntry(tracker: Tracker): void {
|
||||||
return formatDuration(moment.unix(endTime).diff(moment.unix(startTime)));
|
let entry = getRunningEntry(tracker.entries);
|
||||||
|
entry.endTime = moment().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(totalTime: number): string {
|
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
|
||||||
let duration = moment.duration(totalTime);
|
if (entries.contains(toRemove)) {
|
||||||
|
entries.remove(toRemove);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
|
||||||
|
// if we only have one sub entry remaining, we can merge back into our main entry
|
||||||
|
if (entry.subEntries.length == 1) {
|
||||||
|
let single = entry.subEntries[0];
|
||||||
|
entry.startTime = single.startTime;
|
||||||
|
entry.endTime = single.endTime;
|
||||||
|
entry.subEntries = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunning(tracker: Tracker): boolean {
|
||||||
|
return !!getRunningEntry(tracker.entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRunningEntry(entries: Entry[]): Entry {
|
||||||
|
for (let entry of entries) {
|
||||||
|
// if this entry has sub entries, check if one of them is running
|
||||||
|
if (entry.subEntries) {
|
||||||
|
let running = getRunningEntry(entry.subEntries);
|
||||||
|
if (running)
|
||||||
|
return running;
|
||||||
|
} else {
|
||||||
|
// if this entry has no sub entries and no end time, it's running
|
||||||
|
if (!entry.endTime)
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuration(entry: Entry): number {
|
||||||
|
if (entry.subEntries) {
|
||||||
|
return getTotalDuration(entry.subEntries);
|
||||||
|
} else {
|
||||||
|
let endTime = entry.endTime ? moment(entry.endTime) : moment();
|
||||||
|
return endTime.diff(moment(entry.startTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalDuration(entries: Entry[]): number {
|
||||||
|
let ret = 0;
|
||||||
|
for (let entry of entries)
|
||||||
|
ret += getDuration(entry);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
||||||
|
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: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
|
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 {
|
||||||
let ret = "";
|
let ret = "";
|
||||||
if (duration.hours() > 0)
|
let duration = moment.duration(totalTime);
|
||||||
ret += duration.hours() + "h ";
|
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)
|
if (duration.minutes() > 0)
|
||||||
ret += duration.minutes() + "m ";
|
ret += duration.minutes() + "m ";
|
||||||
ret += duration.seconds() + "s";
|
ret += duration.seconds() + "s";
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function 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(createTableRow(entry, settings));
|
table.push(...createTableSection(entry, settings));
|
||||||
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker))}**`]);
|
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
|
||||||
|
|
||||||
let ret = "";
|
let ret = "";
|
||||||
// calculate the width every column needs to look neat when monospaced
|
// calculate the width every column needs to look neat when monospaced
|
||||||
|
@ -208,27 +270,182 @@ function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettin
|
||||||
for (let r = 0; r < table.length; r++) {
|
for (let r = 0; r < table.length; r++) {
|
||||||
// add separators after first row
|
// add separators after first row
|
||||||
if (r == 1)
|
if (r == 1)
|
||||||
ret += Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + "\n";
|
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n";
|
||||||
|
|
||||||
let row: string[] = [];
|
let row: string[] = [];
|
||||||
for (let i = 0; i < 4; i++)
|
for (let i = 0; i < 4; i++)
|
||||||
row.push(table[r][i].padEnd(widths[i], " "));
|
row.push(table[r][i].padEnd(widths[i], " "));
|
||||||
ret += row.join(" | ") + "\n";
|
ret += "| " + row.join(" | ") + " |\n";
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
ret += createTableRow(entry, settings).join(settings.csvDelimiter) + "\n";
|
for (let row of createTableSection(entry, settings))
|
||||||
|
ret += row.join(settings.csvDelimiter) + "\n";
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTableRow(entry: Entry, settings: SimpleTimeTrackerSettings): string[] {
|
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
|
||||||
return [
|
let ret = [[
|
||||||
entry.name,
|
entry.name,
|
||||||
formatTimestamp(entry.startTime, settings),
|
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
|
||||||
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
|
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
|
||||||
entry.endTime ? formatDurationBetween(entry.startTime, entry.endTime) : ""];
|
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 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 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 entryButtons = row.createEl("td");
|
||||||
|
entryButtons.addClass("simple-time-tracker-table-buttons");
|
||||||
|
new ButtonComponent(entryButtons)
|
||||||
|
.setClass("clickable-icon")
|
||||||
|
.setIcon(`lucide-play`)
|
||||||
|
.setTooltip("Continue")
|
||||||
|
.setDisabled(trackerRunning)
|
||||||
|
.onClick(async () => {
|
||||||
|
startSubEntry(entry, newSegmentNameBox.getValue());
|
||||||
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
|
});
|
||||||
|
let editButton = new ButtonComponent(entryButtons)
|
||||||
|
.setClass("clickable-icon")
|
||||||
|
.setTooltip("Edit")
|
||||||
|
.setIcon("lucide-pencil")
|
||||||
|
.onClick(async () => {
|
||||||
|
if (nameField.editing()) {
|
||||||
|
entry.name = nameField.endEdit();
|
||||||
|
startField.endEdit();
|
||||||
|
entry.startTime = startField.getTimestamp();
|
||||||
|
if (!entryRunning) {
|
||||||
|
endField.endEdit();
|
||||||
|
entry.endTime = endField.getTimestamp();
|
||||||
|
}
|
||||||
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
|
editButton.setIcon("lucide-pencil");
|
||||||
|
|
||||||
|
renderNameAsMarkdown(nameField.label, getFile, component);
|
||||||
|
} else {
|
||||||
|
nameField.beginEdit(entry.name);
|
||||||
|
// only allow editing start and end times if we don't have sub entries
|
||||||
|
if (!entry.subEntries) {
|
||||||
|
startField.beginEdit(entry.startTime);
|
||||||
|
if (!entryRunning)
|
||||||
|
endField.beginEdit(entry.endTime);
|
||||||
|
}
|
||||||
|
editButton.setIcon("lucide-check");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
new ButtonComponent(entryButtons)
|
||||||
|
.setClass("clickable-icon")
|
||||||
|
.setTooltip("Remove")
|
||||||
|
.setIcon("lucide-trash")
|
||||||
|
.setDisabled(entryRunning)
|
||||||
|
.onClick(async () => {
|
||||||
|
if (!confirm("Are you sure you want to delete this entry?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeEntry(tracker.entries, entry);
|
||||||
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entry.subEntries) {
|
||||||
|
for (let sub of orderedEntries(entry.subEntries, settings))
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
23
styles.css
23
styles.css
|
@ -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);
|
||||||
|
}
|
||||||
|
|
30
test-vault/.obsidian/core-plugins-migration.json
vendored
Normal file
30
test-vault/.obsidian/core-plugins-migration.json
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": false,
|
||||||
|
"canvas": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"properties": false
|
||||||
|
}
|
3
test-vault/.obsidian/core-plugins.json
vendored
3
test-vault/.obsidian/core-plugins.json
vendored
|
@ -4,6 +4,7 @@
|
||||||
"switcher",
|
"switcher",
|
||||||
"graph",
|
"graph",
|
||||||
"backlink",
|
"backlink",
|
||||||
|
"canvas",
|
||||||
"outgoing-link",
|
"outgoing-link",
|
||||||
"tag-pane",
|
"tag-pane",
|
||||||
"page-preview",
|
"page-preview",
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
"note-composer",
|
"note-composer",
|
||||||
"command-palette",
|
"command-palette",
|
||||||
"editor-status",
|
"editor-status",
|
||||||
"starred",
|
"bookmarks",
|
||||||
"outline",
|
"outline",
|
||||||
"word-count",
|
"word-count",
|
||||||
"file-recovery"
|
"file-recovery"
|
||||||
|
|
3
test-vault/.obsidian/plugins/simple-time-tracker/.gitignore
vendored
Normal file
3
test-vault/.obsidian/plugins/simple-time-tracker/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
!data.json
|
|
@ -1,4 +1,8 @@
|
||||||
{
|
{
|
||||||
"timestampFormat": "YY-MM-DD hh:mm:ss",
|
"timestampFormat": "YY-MM-DD hh:mm:ss",
|
||||||
"csvDelimiter": ","
|
"editableTimestampFormat": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"csvDelimiter": ",",
|
||||||
|
"fineGrainedDurations": true,
|
||||||
|
"reverseSegmentOrder": false,
|
||||||
|
"timestampDurations": true
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"id": "simple-time-tracker",
|
|
||||||
"name": "Super Simple Time Tracker",
|
|
||||||
"version": "0.1.3",
|
|
||||||
"minAppVersion": "0.15.0",
|
|
||||||
"description": "Multi-purpose time trackers for your notes!",
|
|
||||||
"author": "Ellpeck",
|
|
||||||
"authorUrl": "https://ellpeck.de",
|
|
||||||
"isDesktopOnly": false
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
# 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
|
|
||||||
{"entries":[{"name":"Think about project","startTime":1664305777,"endTime":1664308788},{"name":"Create project note","startTime":1664308810,"endTime":1664308815},{"name":"Work on project","startTime":1664308830,"endTime":1664309301},{"name":"Segment 4","startTime":1664364444,"endTime":1664364449},{"name":"Segment 5","startTime":1664364495,"endTime":1664364498},{"name":"Segment 6","startTime":1664458520,"endTime":1664458523},{"name":"Segment 7","startTime":1664460326,"endTime":1664460329}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
Think about project,22-09-27 09:09:37,22-09-27 09:59:48,50m 11s
|
|
||||||
Create project note,22-09-27 10:00:10,22-09-27 10:00:15,5s
|
|
||||||
Work on project,22-09-27 10:00:30,22-09-27 10:08:21,7m 51s
|
|
||||||
Segment 4,22-09-28 01:27:24,22-09-28 01:27:29,5s
|
|
||||||
Segment 5,22-09-28 01:28:15,22-09-28 01:28:18,3s
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
10
test-vault/duration_accumulation_test.md
Normal file
10
test-vault/duration_accumulation_test.md
Normal file
|
@ -0,0 +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
|
||||||
|
{"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}]}]}
|
||||||
|
```
|
4
test-vault/test-markdown.md
Normal file
4
test-vault/test-markdown.md
Normal 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}]}
|
||||||
|
```
|
6
test-vault/test/Cool Project.md
Normal file
6
test-vault/test/Cool Project.md
Normal file
|
@ -0,0 +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
|
||||||
|
{"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}]}
|
||||||
|
```
|
6
test-vault/test/Untitled.canvas
Normal file
6
test-vault/test/Untitled.canvas
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"nodes":[
|
||||||
|
{"type":"file","file":"test/Cool Project.md","id":"e41a2deb229880a8","x":-720,"y":-620,"width":1120,"height":1000}
|
||||||
|
],
|
||||||
|
"edges":[]
|
||||||
|
}
|
|
@ -3,5 +3,13 @@
|
||||||
"0.1.0": "0.15.0",
|
"0.1.0": "0.15.0",
|
||||||
"0.1.1": "0.15.0",
|
"0.1.1": "0.15.0",
|
||||||
"0.1.2": "0.15.0",
|
"0.1.2": "0.15.0",
|
||||||
"0.1.3": "0.15.0"
|
"0.1.3": "0.15.0",
|
||||||
|
"0.1.4": "0.15.0",
|
||||||
|
"0.1.5": "0.15.0",
|
||||||
|
"0.1.6": "0.15.0",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue