Compare commits

..

No commits in common. "master" and "0.1.7" have entirely different histories.

24 changed files with 1089 additions and 345 deletions

View file

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

49
.gitignore vendored
View file

@ -1,26 +1,23 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
/main.js
# Exclude local settings
data.json
# Exclude sourcemaps
*.map
# obsidian
workspace
workspace.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
/main.js
# Exclude sourcemaps
*.map
# obsidian
workspace
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.
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
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.
# 🛣️ 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
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-wide.png)](https://ellpeck.de/support)
[![Support me (if you want), via Patreon, Ko-fi or GitHub Sponsors](https://ellpeck.de/res/generalsupport.png)](https://ellpeck.de/support)

View file

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

4
package-lock.json generated
View file

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

View file

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

View file

@ -1,7 +1,7 @@
import {MarkdownRenderChild, Plugin, TFile} from "obsidian";
import { Plugin } from "obsidian";
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
import { displayTracker, loadTracker } from "./tracker";
import { displayTracker, loadTracker, Tracker } from "./tracker";
export default class SimpleTimeTrackerPlugin extends Plugin {
@ -13,28 +13,9 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
this.addSettingTab(new SimpleTimeTrackerSettingsTab(this.app, this));
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
let tracker: Tracker = loadTracker(s);
e.empty();
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)
displayTracker(tracker, e, i.sourcePath, () => i.getSectionInfo(e), this.settings);
});
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 {defaultSettings} from "./settings";
import { defaultSettings } from "./settings";
export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
@ -52,38 +52,12 @@ export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
});
});
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("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"
});
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"
});
}
}

View file

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

View file

@ -1,5 +1,5 @@
import {moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild} from "obsidian";
import {SimpleTimeTrackerSettings} from "./settings";
import { moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile } from "obsidian";
import { SimpleTimeTrackerSettings } from "./settings";
export interface Tracker {
entries: Entry[];
@ -7,8 +7,8 @@ export interface Tracker {
export interface Entry {
name: string;
startTime: string;
endTime: string;
startTime: number;
endTime: number;
subEntries: Entry[];
}
@ -31,9 +31,7 @@ export async function saveTracker(tracker: Tracker, app: App, fileName: string,
export function loadTracker(json: string): Tracker {
if (json) {
try {
let ret = JSON.parse(json);
fixLegacyTimestamps(ret.entries);
return ret;
return JSON.parse(json);
} catch (e) {
console.log(`Failed to parse Tracker from ${json}`);
}
@ -41,11 +39,7 @@ export function loadTracker(json: string): Tracker {
return {entries: []};
}
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");
export function displayTracker(tracker: Tracker, element: HTMLElement, file: string, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings): void {
// add start/stop controls
let running = isRunning(tracker);
let btn = new ButtonComponent(element)
@ -58,7 +52,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
} else {
startNewEntry(tracker, newSegmentNameBox.getValue());
}
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
await saveTracker(tracker, this.app, file, getSectionInfo());
});
btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new TextComponent(element)
@ -85,8 +79,8 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
createEl("th", {text: "Duration"}),
createEl("th"));
for (let entry of orderedEntries(tracker.entries, settings))
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
for (let entry of tracker.entries)
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, file, getSectionInfo, settings, 0);
// add copy buttons
let buttons = element.createEl("div", {cls: "simple-time-tracker-bottom"});
@ -110,7 +104,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
}, 1000);
}
function startSubEntry(entry: Entry, name: string): void {
function startSubEntry(entry: Entry, name: string) {
// if this entry is not split yet, we add its time as a sub-entry instead
if (!entry.subEntries) {
entry.subEntries = [{...entry, name: `Part 1`}];
@ -120,19 +114,19 @@ function startSubEntry(entry: Entry, name: string): void {
if (!name)
name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({name: name, startTime: moment().toISOString(), endTime: null, subEntries: null});
entry.subEntries.push({name: name, startTime: moment().unix(), endTime: null, subEntries: null});
}
function startNewEntry(tracker: Tracker, name: string): void {
if (!name)
name = `Segment ${tracker.entries.length + 1}`;
let entry: Entry = {name: name, startTime: moment().toISOString(), endTime: null, subEntries: null};
let entry: Entry = {name: name, startTime: moment().unix(), endTime: null, subEntries: null};
tracker.entries.push(entry);
}
function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().toISOString();
entry.endTime = moment().unix();
}
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
@ -176,12 +170,12 @@ function getRunningEntry(entries: Entry[]): Entry {
return null;
}
function getDuration(entry: Entry): number {
function getDuration(entry: Entry) {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
let endTime = entry.endTime ? moment(entry.endTime) : moment();
return endTime.diff(moment(entry.startTime));
let endTime = entry.endTime ? moment.unix(entry.endTime) : moment();
return endTime.diff(moment.unix(entry.startTime));
}
}
@ -192,7 +186,7 @@ function getTotalDuration(entries: Entry[]): number {
return ret;
}
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings) {
let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running), settings));
@ -203,64 +197,36 @@ function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLE
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 formatTimestamp(timestamp: number, settings: SimpleTimeTrackerSettings): string {
return moment.unix(timestamp).format(settings.timestampFormat);
}
function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
let ret = "";
let duration = moment.duration(totalTime);
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
if (settings.timestampDurations) {
if (settings.fineGrainedDurations) {
let days = Math.floor(duration.asDays());
if (days > 0)
ret += days + ".";
}
ret += `${hours.toString().padStart(2, "0")}:${duration.minutes().toString().padStart(2, "0")}:${duration.seconds().toString().padStart(2, "0")}`;
let hours: number;
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 {
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";
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 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 {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of orderedEntries(tracker.entries, settings))
for (let entry of tracker.entries)
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
@ -282,7 +248,7 @@ function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettin
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of orderedEntries(tracker.entries, settings)) {
for (let entry of tracker.entries) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
@ -290,70 +256,60 @@ function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): strin
}
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
let ret = [[
let ret: string[][] = [[
entry.name,
entry.startTime ? formatTimestamp(entry.startTime, settings) : "",
entry.endTime ? formatTimestamp(entry.endTime, settings) : "",
entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""]];
if (entry.subEntries) {
for (let sub of orderedEntries(entry.subEntries, settings))
for (let sub of entry.subEntries)
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;
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, running: boolean, file: string, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number) {
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);
let name = row.createEl("td");
let namePar = name.createEl("span", {text: entry.name});
namePar.style.marginLeft = `${indent}em`;
let nameBox = new TextComponent(name).setValue(entry.name);
nameBox.inputEl.hidden = true;
row.createEl("td", {text: 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) : ""});
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());
});
if (!running) {
new ButtonComponent(entryButtons)
.setClass("clickable-icon")
.setIcon(`lucide-play`)
.setTooltip("Continue")
.onClick(async () => {
startSubEntry(entry, newSegmentNameBox.getValue());
await saveTracker(tracker, this.app, file, 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());
if (namePar.hidden) {
namePar.hidden = false;
nameBox.inputEl.hidden = true;
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);
if (nameBox.getValue()) {
entry.name = nameBox.getValue();
namePar.setText(entry.name);
await saveTracker(tracker, this.app, file, getSectionInfo());
}
} else {
namePar.hidden = true;
nameBox.inputEl.hidden = false;
nameBox.setValue(entry.name);
editButton.setIcon("lucide-check");
}
});
@ -361,91 +317,13 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
.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());
await saveTracker(tracker, this.app, file, 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;
}
for (let sub of entry.subEntries)
addEditableTableRow(tracker, sub, table, newSegmentNameBox, running, file, getSectionInfo, settings, indent + 1);
}
}

View file

@ -1,9 +1,6 @@
.simple-time-tracker-container {
overflow-x: scroll;
}
.simple-time-tracker-settings-image {
width: 100%;
.simple-time-tracker-support {
max-width: 50%;
width: 400px;
height: auto;
}
@ -59,23 +56,9 @@
.simple-time-tracker-table td,
.simple-time-tracker-table th {
vertical-align: middle;
border: none;
}
.simple-time-tracker-table .clickable-icon {
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,6 +25,5 @@
"publish": false,
"sync": false,
"canvas": true,
"bookmarks": true,
"properties": false
"bookmarks": true
}

View file

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

View file

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

View file

@ -0,0 +1,404 @@
/*
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

@ -0,0 +1,404 @@
/*
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

@ -0,0 +1,10 @@
{
"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

@ -0,0 +1,10 @@
{
"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

@ -0,0 +1,64 @@
.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

@ -0,0 +1,64 @@
.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,9 +2,5 @@
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}]}]}
{"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}]}
```

View file

@ -1,4 +0,0 @@
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.
```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}]}
{"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}]}
```

View file

@ -7,9 +7,5 @@
"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"
"0.1.7": "1.2.8"
}