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 # top-most EditorConfig file
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
tab_width = 4 tab_width = 4

49
.gitignore vendored
View file

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

View file

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

4
package-lock.json generated
View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import {App, PluginSettingTab, Setting} from "obsidian"; import { App, PluginSettingTab, Setting } from "obsidian";
import SimpleTimeTrackerPlugin from "./main"; import SimpleTimeTrackerPlugin from "./main";
import {defaultSettings} from "./settings"; import { defaultSettings } from "./settings";
export class SimpleTimeTrackerSettingsTab extends PluginSettingTab { export class SimpleTimeTrackerSettingsTab extends PluginSettingTab {
@ -52,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("hr");
this.containerEl.createEl("p", { text: "Need help using the plugin? Feel free to join the Discord server!" }); 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://link.ellpeck.de/discordweb" }).createEl("img", { this.containerEl.createEl("a", {href: "https://ellpeck.de/support"})
attr: { src: "https://ellpeck.de/res/discord-wide.png" }, .createEl("img", {
cls: "simple-time-tracker-settings-image" attr: {src: "https://ellpeck.de/res/generalsupport.png"},
}); cls: "simple-time-tracker-support"
this.containerEl.createEl("p", { text: "If you like this plugin and want to support its development, you can do so through my website by clicking this fancy image!" }); });
this.containerEl.createEl("a", { href: "https://ellpeck.de/support" }).createEl("img", {
attr: { src: "https://ellpeck.de/res/generalsupport-wide.png" },
cls: "simple-time-tracker-settings-image"
});
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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! More notes for my cool project! This note shows that we can correctly display accumulated time that lasts longer than a 24 hour day!
```simple-time-tracker ```simple-time-tracker
{"entries":[{"name":"test","startTime":"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}]}]} {"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}]}
```
```simple-time-tracker
{"entries":[{"name":"Segment 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1","startTime":"2024-02-26T13:37:59.292Z","endTime":"2024-02-26T13:38:01.437Z","subEntries":null},{"name":"Part 2","startTime":"2024-02-26T14:04:14.156Z","endTime":"2024-02-26T14:04:30.576Z","subEntries":null}]},{"name":"Part 2","startTime":"2024-02-26T13:38:16.235Z","endTime":"2024-02-26T13:38:18.895Z","subEntries":null}]}]}
``` ```

View file

@ -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. These are the notes for my cool project. There's so much left to do! I wish I had a way to track the amount of time I spend on each part of the project.
```simple-time-tracker ```simple-time-tracker
{"entries":[{"name":"Segment 1","startTime":"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.4": "0.15.0",
"0.1.5": "0.15.0", "0.1.5": "0.15.0",
"0.1.6": "0.15.0", "0.1.6": "0.15.0",
"0.1.7": "1.2.8", "0.1.7": "1.2.8"
"0.1.8": "1.3.0",
"0.2.0": "1.3.0",
"0.2.1": "1.3.0",
"0.2.2": "1.3.0"
} }