Compare commits

...

14 commits

Author SHA1 Message Date
Ell
993a7a4995 1.0.3 2024-08-09 19:56:59 +02:00
Ell
7b189a493a fixed arrows not being displayed for markdown content 2024-08-09 19:55:54 +02:00
Ell
ea4f51b1e9 1.0.2 2024-08-09 19:45:45 +02:00
Ell
321f0178d4 moved around async code 2024-08-09 19:41:11 +02:00
Ell
07c083d63f 1.0.1 2024-08-09 19:28:38 +02:00
Ell
d80bc764ce automatically remove null sub-entries from existing trackers 2024-08-09 19:27:29 +02:00
Ell
0ca60318f8 improve async rendering code 2024-08-09 19:24:10 +02:00
Ell
6820640a4f made sub-entries optional 2024-08-09 19:05:16 +02:00
Ell
41d595201d 1.0.0 2024-08-09 18:59:10 +02:00
Ell
ed0ad218b9 improved collapse styling 2024-08-09 18:56:04 +02:00
James Tan
1ddefa51f4
feat: Implement collapsing nested subentries (#50)
* feat: Implement collapsing nested subentries

* fix: Change tooltip based on collapse state

* fix: Avoid hardcoded CSS constants

* fix: Retain old formatting when editing row

* fix: Change collapsed field from number to boolean

* fix: Remove unneeded wrapper code
2024-08-09 18:26:15 +02:00
Ell
9b718d2cae fill out readme links 2024-08-09 18:25:23 +02:00
Ell
501cc50bf6 small fixes 2024-08-09 18:23:28 +02:00
Ell
cbba377c60 added a dataview api
closes #4
2024-08-09 18:22:37 +02:00
14 changed files with 340 additions and 187 deletions

View file

@ -12,6 +12,36 @@ 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)
## 🔍 Tracker Data in Dataview
Super Simple Time Tracker has a public API that can be used with [Dataview](https://blacksmithgu.github.io/obsidian-dataview/), specifically [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/api/intro/), which can be accessed using the following code:
```js
dv.app.plugins.plugins["simple-time-tracker"].api;
```
The following is a short example that uses DataviewJS to load all trackers in the vault and print the total duration of each tracker. You can also find this example in action [in the test vault](https://github.com/Ellpeck/ObsidianSimpleTimeTracker/blob/master/test-vault/dataview-test.md?plain=1).
```js
// get the time tracker plugin api instance
let api = dv.app.plugins.plugins["simple-time-tracker"].api;
for (let page of dv.pages()) {
// load trackers in the file with the given path
let trackers = await api.loadAllTrackers(page.file.path);
if (trackers.length)
dv.el("strong", "Trackers in " + page.file.name);
for (let { section, tracker } of trackers) {
// print the total duration of the tracker
let duration = api.getTotalDuration(tracker.entries);
dv.el("p", api.formatDuration(duration));
}
}
```
A full list of the functions exposed through the API can be found [in the code](https://github.com/Ellpeck/ObsidianSimpleTimeTracker/blob/master/src/main.ts#L8-L16). Proper documentation for the API will be added in the future.
# 👀 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.

View file

@ -1,7 +1,7 @@
{
"id": "simple-time-tracker",
"name": "Super Simple Time Tracker",
"version": "0.2.2",
"version": "1.0.3",
"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": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "simple-time-tracker",
"version": "0.2.2",
"version": "1.0.3",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

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

View file

@ -1,11 +1,20 @@
import {MarkdownRenderChild, Plugin, TFile} from "obsidian";
import { MarkdownRenderChild, Plugin, TFile } from "obsidian";
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
import { displayTracker, loadTracker } from "./tracker";
import { displayTracker, Entry, formatDuration, formatTimestamp, getDuration, getRunningEntry, getTotalDuration, isRunning, loadAllTrackers, loadTracker, orderedEntries } from "./tracker";
export default class SimpleTimeTrackerPlugin extends Plugin {
settings: SimpleTimeTrackerSettings;
public api = {
// verbatim versions of the functions found in tracker.ts with the same parameters
loadTracker, loadAllTrackers, getDuration, getTotalDuration, getRunningEntry, isRunning,
// modified versions of the functions found in tracker.ts, with the number of required arguments reduced
formatTimestamp: (timestamp: string) => formatTimestamp(timestamp, this.settings),
formatDuration: (totalTime: number) => formatDuration(totalTime, this.settings),
orderedEntries: (entries: Entry[]) => orderedEntries(entries, this.settings)
};
public settings: SimpleTimeTrackerSettings;
async onload(): Promise<void> {
await this.loadSettings();
@ -14,27 +23,22 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
e.empty();
let component = new MarkdownRenderChild(e)
let component = new MarkdownRenderChild(e);
let tracker = loadTracker(s);
// Initial file name
// Wrap file name in a function since it can change
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) => {
component.registerEvent(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(this.app, tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
i.addChild(component)
displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
i.addChild(component);
});
this.addCommand({

View file

@ -1,5 +1,5 @@
import {moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild} from "obsidian";
import {SimpleTimeTrackerSettings} from "./settings";
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian";
import { SimpleTimeTrackerSettings } from "./settings";
import { ConfirmModal } from "./confirm-modal";
export interface Tracker {
@ -10,10 +10,11 @@ export interface Entry {
name: string;
startTime: string;
endTime: string;
subEntries: Entry[];
subEntries?: Entry[];
collapsed?: boolean;
}
export async function saveTracker(tracker: Tracker, app: App, fileName: string, section: MarkdownSectionInformation): Promise<void> {
export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> {
let file = app.vault.getAbstractFileByPath(fileName) as TFile;
if (!file)
return;
@ -33,19 +34,43 @@ export function loadTracker(json: string): Tracker {
if (json) {
try {
let ret = JSON.parse(json);
fixLegacyTimestamps(ret.entries);
updateLegacyInfo(ret.entries);
return ret;
} catch (e) {
console.log(`Failed to parse Tracker from ${json}`);
}
}
return {entries: []};
return { entries: [] };
}
export async function loadAllTrackers(fileName: string): Promise<{ section: MarkdownSectionInformation, tracker: Tracker }[]> {
let file = app.vault.getAbstractFileByPath(fileName);
let content = (await app.vault.cachedRead(file as TFile)).split("\n");
let trackers: { section: MarkdownSectionInformation, tracker: Tracker }[] = [];
let curr: Partial<MarkdownSectionInformation> | undefined;
for (let i = 0; i < content.length; i++) {
let line = content[i];
if (line.trimEnd() == "```simple-time-tracker") {
curr = { lineStart: i + 1, text: "" };
} else if (curr) {
if (line.trimEnd() == "```") {
curr.lineEnd = i - 1;
let tracker = loadTracker(curr.text);
trackers.push({ section: curr as MarkdownSectionInformation, tracker: tracker });
curr = undefined;
} else {
curr.text += `${line}\n`;
}
}
}
return trackers;
}
type GetFile = () => string;
export function displayTracker(app: App, tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
element.addClass("simple-time-tracker-container");
// add start/stop controls
let running = isRunning(tracker);
@ -59,7 +84,7 @@ export function displayTracker(app: App, tracker: Tracker, element: HTMLElement,
} else {
startNewEntry(tracker, newSegmentNameBox.getValue());
}
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
await saveTracker(tracker, getFile(), getSectionInfo());
});
btn.buttonEl.addClass("simple-time-tracker-btn");
let newSegmentNameBox = new TextComponent(element)
@ -68,29 +93,29 @@ export function displayTracker(app: App, tracker: Tracker, element: HTMLElement,
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
// add timers
let timer = element.createDiv({cls: "simple-time-tracker-timers"});
let currentDiv = timer.createEl("div", {cls: "simple-time-tracker-timer"});
let current = currentDiv.createEl("span", {cls: "simple-time-tracker-timer-time"});
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"});
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) {
// add table
let table = element.createEl("table", {cls: "simple-time-tracker-table"});
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", { text: "Segment" }),
createEl("th", { text: "Start time" }),
createEl("th", { text: "End time" }),
createEl("th", { text: "Duration" }),
createEl("th"));
for (let entry of orderedEntries(tracker.entries, settings))
addEditableTableRow(app, tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
// add copy buttons
let buttons = element.createEl("div", {cls: "simple-time-tracker-bottom"});
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
new ButtonComponent(buttons)
.setButtonText("Copy as table")
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
@ -111,57 +136,27 @@ export function displayTracker(app: App, tracker: Tracker, element: HTMLElement,
}, 1000);
}
function startSubEntry(entry: Entry, name: string): void {
// 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`}];
entry.startTime = null;
entry.endTime = null;
}
if (!name)
name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({name: name, startTime: moment().toISOString(), 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};
tracker.entries.push(entry);
}
function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().toISOString();
}
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
if (entries.contains(toRemove)) {
entries.remove(toRemove);
return true;
export function getDuration(entry: Entry): number {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
for (let entry of entries) {
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
// if we only have one sub entry remaining, we can merge back into our main entry
if (entry.subEntries.length == 1) {
let single = entry.subEntries[0];
entry.startTime = single.startTime;
entry.endTime = single.endTime;
entry.subEntries = null;
}
return true;
}
}
let endTime = entry.endTime ? moment(entry.endTime) : moment();
return endTime.diff(moment(entry.startTime));
}
return false;
}
function isRunning(tracker: Tracker): boolean {
export function getTotalDuration(entries: Entry[]): number {
let ret = 0;
for (let entry of entries)
ret += getDuration(entry);
return ret;
}
export function isRunning(tracker: Tracker): boolean {
return !!getRunningEntry(tracker.entries);
}
function getRunningEntry(entries: Entry[]): Entry {
export function getRunningEntry(entries: Entry[]): Entry {
for (let entry of entries) {
// if this entry has sub entries, check if one of them is running
if (entry.subEntries) {
@ -177,46 +172,46 @@ function getRunningEntry(entries: Entry[]): Entry {
return null;
}
function getDuration(entry: Entry): number {
if (entry.subEntries) {
return getTotalDuration(entry.subEntries);
} else {
let endTime = entry.endTime ? moment(entry.endTime) : moment();
return endTime.diff(moment(entry.startTime));
}
}
export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let table = [["Segment", "Start time", "End time", "Duration"]];
for (let entry of orderedEntries(tracker.entries, settings))
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
function getTotalDuration(entries: Entry[]): number {
let ret = 0;
for (let entry of entries)
ret += getDuration(entry);
let ret = "";
// calculate the width every column needs to look neat when monospaced
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++) {
// add separators after first row
if (r == 1)
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n";
let row: string[] = [];
for (let i = 0; i < 4; i++)
row.push(table[r][i].padEnd(widths[i], " "));
ret += "| " + row.join(" | ") + " |\n";
}
return ret;
}
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running), settings));
currentDiv.hidden = false;
} else {
currentDiv.hidden = true;
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of orderedEntries(tracker.entries, settings)) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
return ret;
}
function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
}
export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment(timestamp).format(settings.timestampFormat);
}
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
return moment(timestamp).format(settings.editableTimestampFormat);
}
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
return moment(formatted, settings.editableTimestampFormat).toISOString();
}
function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
let ret = "";
let duration = moment.duration(totalTime);
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
@ -247,48 +242,89 @@ function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings):
return ret;
}
function fixLegacyTimestamps(entries: Entry[]): void {
function startSubEntry(entry: Entry, name: string): void {
// 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` }];
entry.startTime = null;
entry.endTime = null;
}
if (!name)
name = `Part ${entry.subEntries.length + 1}`;
entry.subEntries.push({ name: name, startTime: moment().toISOString(), endTime: null, subEntries: undefined });
}
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: undefined };
tracker.entries.push(entry);
}
function endRunningEntry(tracker: Tracker): void {
let entry = getRunningEntry(tracker.entries);
entry.endTime = moment().toISOString();
}
function removeEntry(entries: Entry[], toRemove: Entry): boolean {
if (entries.contains(toRemove)) {
entries.remove(toRemove);
return true;
} else {
for (let entry of entries) {
if (entry.subEntries && removeEntry(entry.subEntries, toRemove)) {
// if we only have one sub entry remaining, we can merge back into our main entry
if (entry.subEntries.length == 1) {
let single = entry.subEntries[0];
entry.startTime = single.startTime;
entry.endTime = single.endTime;
entry.subEntries = undefined;
}
return true;
}
}
}
return false;
}
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
let running = getRunningEntry(tracker.entries);
if (running && !running.endTime) {
current.setText(formatDuration(getDuration(running), settings));
currentDiv.hidden = false;
} else {
currentDiv.hidden = true;
}
total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
}
function 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 updateLegacyInfo(entries: Entry[]): void {
for (let entry of entries) {
// in 0.1.8, timestamps were changed from unix to iso
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();
// in 1.0.0, sub-entries were made optional
if (entry.subEntries == null || !entry.subEntries.length)
entry.subEntries = undefined;
if (entry.subEntries)
fixLegacyTimestamps(entry.subEntries);
updateLegacyInfo(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))
table.push(...createTableSection(entry, settings));
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
let ret = "";
// calculate the width every column needs to look neat when monospaced
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++) {
// add separators after first row
if (r == 1)
ret += "| " + Array.from(Array(4).keys()).map(i => "-".repeat(widths[i])).join(" | ") + " |\n";
let row: string[] = [];
for (let i = 0; i < 4; i++)
row.push(table[r][i].padEnd(widths[i], " "));
ret += "| " + row.join(" | ") + " |\n";
}
return ret;
}
function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
let ret = "";
for (let entry of orderedEntries(tracker.entries, settings)) {
for (let row of createTableSection(entry, settings))
ret += row.join(settings.csvDelimiter) + "\n";
}
return ret;
}
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
let ret = [[
@ -303,11 +339,7 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings):
return ret;
}
function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
}
function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
let entryRunning = getRunningEntry(tracker.entries) == entry;
let row = table.createEl("tr");
@ -315,10 +347,25 @@ function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HT
let startField = new EditableTimestampField(row, (entry.startTime), settings);
let endField = new EditableTimestampField(row, (entry.endTime), settings);
row.createEl("td", {text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : ""});
row.createEl("td", { text: entry.endTime || entry.subEntries ? formatDuration(getDuration(entry), settings) : "" });
renderNameAsMarkdown(nameField.label, getFile, component);
let expandButton = new ButtonComponent(nameField.label)
.setClass("clickable-icon")
.setClass("simple-time-tracker-expand-button")
.setIcon(`chevron-${entry.collapsed ? "left" : "down"}`)
.onClick(async () => {
if (entry.collapsed) {
entry.collapsed = undefined;
} else {
entry.collapsed = true;
}
await saveTracker(tracker, getFile(), getSectionInfo());
});
if (!entry.subEntries)
expandButton.buttonEl.style.visibility = "hidden";
let entryButtons = row.createEl("td");
entryButtons.addClass("simple-time-tracker-table-buttons");
new ButtonComponent(entryButtons)
@ -328,7 +375,7 @@ function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HT
.setDisabled(trackerRunning)
.onClick(async () => {
startSubEntry(entry, newSegmentNameBox.getValue());
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
await saveTracker(tracker, getFile(), getSectionInfo());
});
let editButton = new ButtonComponent(entryButtons)
.setClass("clickable-icon")
@ -337,18 +384,20 @@ function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HT
.onClick(async () => {
if (nameField.editing()) {
entry.name = nameField.endEdit();
expandButton.buttonEl.style.display = null;
startField.endEdit();
entry.startTime = startField.getTimestamp();
if (!entryRunning) {
endField.endEdit();
entry.endTime = endField.getTimestamp();
}
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
await saveTracker(tracker, getFile(), getSectionInfo());
editButton.setIcon("lucide-pencil");
renderNameAsMarkdown(nameField.label, getFile, component);
} else {
nameField.beginEdit(entry.name);
expandButton.buttonEl.style.display = "none";
// only allow editing start and end times if we don't have sub entries
if (!entry.subEntries) {
startField.beginEdit(entry.startTime);
@ -365,23 +414,23 @@ function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HT
.setDisabled(entryRunning)
.onClick(async () => {
const confirmed = await showConfirm(app, "Are you sure you want to delete this entry?")
const confirmed = await showConfirm("Are you sure you want to delete this entry?");
if (!confirmed) {
return;
}
removeEntry(tracker.entries, entry);
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
await saveTracker(tracker, getFile(), getSectionInfo());
});
if (entry.subEntries) {
if (entry.subEntries && !entry.collapsed) {
for (let sub of orderedEntries(entry.subEntries, settings))
addEditableTableRow(app, tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
}
}
function showConfirm(app: App, message: string): Promise<boolean> {
function showConfirm(message: string): Promise<boolean> {
return new Promise((resolve) => {
const modal = new ConfirmModal(app, message, resolve);
modal.open();
@ -389,6 +438,7 @@ function addEditableTableRow(app: App, tracker: Tracker, entry: Entry, table: HT
}
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
// we don't have to wait here since async code only occurs when a file needs to be loaded (like a linked image)
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
// rendering wraps it in a paragraph
label.innerHTML = label.querySelector("p").innerHTML;
@ -402,7 +452,7 @@ class EditableField {
constructor(row: HTMLTableRowElement, indent: number, value: string) {
this.cell = row.createEl("td");
this.label = this.cell.createEl("span", {text: value});
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");

View file

@ -64,7 +64,12 @@
}
.simple-time-tracker-table .clickable-icon {
display: inline;
display: inline-block;
vertical-align: middle;
}
.simple-time-tracker-expand-button {
margin-inline-start: 0.5em;
}
.simple-time-tracker-input {

View file

@ -1,3 +1,4 @@
[
"simple-time-tracker"
"simple-time-tracker",
"dataview"
]

View file

@ -1,20 +1,30 @@
[
"file-explorer",
"global-search",
"switcher",
"graph",
"backlink",
"canvas",
"outgoing-link",
"tag-pane",
"page-preview",
"daily-notes",
"templates",
"note-composer",
"command-palette",
"editor-status",
"bookmarks",
"outline",
"word-count",
"file-recovery"
]
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"outgoing-link": true,
"tag-pane": true,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": false,
"canvas": true,
"bookmarks": true,
"properties": false
}

View file

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

View file

@ -0,0 +1,27 @@
{
"renderNullAs": "\\-",
"taskCompletionTracking": false,
"taskCompletionUseEmojiShorthand": false,
"taskCompletionText": "completion",
"taskCompletionDateFormat": "yyyy-MM-dd",
"recursiveSubTaskCompletion": false,
"warnOnEmptyResult": true,
"refreshEnabled": true,
"refreshInterval": 2500,
"defaultDateFormat": "MMMM dd, yyyy",
"defaultDateTimeFormat": "h:mm a - MMMM dd, yyyy",
"maxRecursiveRenderDepth": 4,
"tableIdColumnName": "File",
"tableGroupColumnName": "Group",
"showResultCount": true,
"allowHtml": true,
"inlineQueryPrefix": "=",
"inlineJsQueryPrefix": "$=",
"inlineQueriesInCodeblocks": true,
"enableInlineDataview": true,
"enableDataviewJs": true,
"enableInlineDataviewJs": true,
"prettyRenderInlineFields": true,
"prettyRenderInlineFieldsInLivePreview": true,
"dataviewJsKeyword": "dataviewjs"
}

View file

@ -0,0 +1,18 @@
```dataviewjs
// get the time tracker plugin api instance
let api = dv.app.plugins.plugins["simple-time-tracker"].api;
for(let page of dv.pages()) {
// load trackers in the file with the given path
let trackers = await api.loadAllTrackers(page.file.path);
if (trackers.length)
dv.el("strong", "Trackers in " + page.file.name);
for (let {section, tracker} of trackers) {
// print the total duration of the tracker
let duration = api.getTotalDuration(tracker.entries);
dv.el("p", api.formatDuration(duration))
}
}
```

View file

@ -1,4 +1,5 @@
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}]}
{"entries":[{"name":"`Segment 1`","startTime":"2022-09-27T19:51:18.000Z","endTime":"2022-09-27T19:51:24.000Z"},{"name":"Segment 2","startTime":"2022-09-27T19:51:25.000Z","endTime":"2022-09-27T19:51:26.000Z"},{"name":"#tag Seqment 3 *add* #tag1 text","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1 #tagp1","startTime":"2024-03-17T11:16:00.382Z","endTime":"2024-03-17T11:16:15.966Z"},{"name":"Part 3","startTime":"2024-03-17T11:17:08.000Z","endTime":"2024-03-17T11:17:24.000Z"}]},{"name":"#tag3 Segment 4","startTime":null,"endTime":null,"subEntries":[{"name":"Part 1 #tag4","startTime":"2024-03-17T12:22:04.000Z","endTime":"2024-03-17T12:22:16.000Z"},{"name":"#tag5 Part 2 *italic*","startTime":"2024-03-17T12:22:20.000Z","endTime":"2024-03-17T12:22:24.000Z"}]},{"name":"*italic* Segment 5 #tag6 [test2](test2)","startTime":"2024-03-17T12:40:37.000Z","endTime":"2024-03-17T12:40:45.000Z"},{"name":"Segment 6","startTime":"2024-03-27T13:20:56.000Z","endTime":"2024-08-09T16:27:18.029Z"}]}
```

View file

@ -11,5 +11,9 @@
"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.2.2": "1.3.0",
"1.0.0": "1.3.0",
"1.0.1": "1.3.0",
"1.0.2": "1.3.0",
"1.0.3": "1.3.0"
}