mirror of
https://github.com/Ellpeck/ObsidianSimpleTimeTracker.git
synced 2024-11-26 19:18:34 +01:00
Compare commits
No commits in common. "993a7a499516ddce0bbdfa6a4eb89a9dd46c5f8b" and "5b79425a50b9c15d3f6624b6ecfef8ebfa80d330" have entirely different histories.
993a7a4995
...
5b79425a50
14 changed files with 186 additions and 339 deletions
30
README.md
30
README.md
|
@ -12,36 +12,6 @@ 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)
|
[![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
|
# 👀 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.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "simple-time-tracker",
|
"id": "simple-time-tracker",
|
||||||
"name": "Super Simple Time Tracker",
|
"name": "Super Simple Time Tracker",
|
||||||
"version": "1.0.3",
|
"version": "0.2.2",
|
||||||
"minAppVersion": "1.2.8",
|
"minAppVersion": "1.2.8",
|
||||||
"description": "Multi-purpose time trackers for your notes!",
|
"description": "Multi-purpose time trackers for your notes!",
|
||||||
"author": "Ellpeck",
|
"author": "Ellpeck",
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "simple-time-tracker",
|
"name": "simple-time-tracker",
|
||||||
"version": "1.0.3",
|
"version": "0.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "simple-time-tracker",
|
"name": "simple-time-tracker",
|
||||||
"version": "1.0.3",
|
"version": "0.2.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "simple-time-tracker",
|
"name": "simple-time-tracker",
|
||||||
"version": "1.0.3",
|
"version": "0.2.2",
|
||||||
"description": "Multi-purpose time trackers for your notes!",
|
"description": "Multi-purpose time trackers for your notes!",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
32
src/main.ts
32
src/main.ts
|
@ -1,20 +1,11 @@
|
||||||
import { MarkdownRenderChild, Plugin, TFile } from "obsidian";
|
import {MarkdownRenderChild, Plugin, TFile} from "obsidian";
|
||||||
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
|
import { defaultSettings, SimpleTimeTrackerSettings } from "./settings";
|
||||||
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
|
import { SimpleTimeTrackerSettingsTab } from "./settings-tab";
|
||||||
import { displayTracker, Entry, formatDuration, formatTimestamp, getDuration, getRunningEntry, getTotalDuration, isRunning, loadAllTrackers, loadTracker, orderedEntries } from "./tracker";
|
import { displayTracker, loadTracker } from "./tracker";
|
||||||
|
|
||||||
export default class SimpleTimeTrackerPlugin extends Plugin {
|
export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
|
|
||||||
public api = {
|
settings: SimpleTimeTrackerSettings;
|
||||||
// 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> {
|
async onload(): Promise<void> {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
@ -23,22 +14,27 @@ export default class SimpleTimeTrackerPlugin extends Plugin {
|
||||||
|
|
||||||
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
|
this.registerMarkdownCodeBlockProcessor("simple-time-tracker", (s, e, i) => {
|
||||||
e.empty();
|
e.empty();
|
||||||
let component = new MarkdownRenderChild(e);
|
let component = new MarkdownRenderChild(e)
|
||||||
let tracker = loadTracker(s);
|
let tracker = loadTracker(s);
|
||||||
|
|
||||||
// Wrap file name in a function since it can change
|
// Initial file name
|
||||||
let filePath = i.sourcePath;
|
let filePath = i.sourcePath;
|
||||||
|
|
||||||
|
// Getter passed to displayTracker since the file name can change
|
||||||
const getFile = () => filePath;
|
const getFile = () => filePath;
|
||||||
|
|
||||||
// Hook rename events to update the file path
|
// Hook rename events to update the file path
|
||||||
component.registerEvent(this.app.vault.on("rename", (file, oldPath) => {
|
const renameEventRef = this.app.vault.on("rename", (file, oldPath) => {
|
||||||
if (file instanceof TFile && oldPath === filePath) {
|
if (file instanceof TFile && oldPath === filePath) {
|
||||||
filePath = file.path;
|
filePath = file.path;
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
|
||||||
displayTracker(tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
|
// Register the event to remove on unload
|
||||||
i.addChild(component);
|
component.registerEvent(renameEventRef);
|
||||||
|
|
||||||
|
displayTracker(this.app, tracker, e, getFile, () => i.getSectionInfo(e), this.settings, component);
|
||||||
|
i.addChild(component)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
|
|
338
src/tracker.ts
338
src/tracker.ts
|
@ -1,5 +1,5 @@
|
||||||
import { moment, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild } from "obsidian";
|
import {moment, App, MarkdownSectionInformation, ButtonComponent, TextComponent, TFile, MarkdownRenderer, Component, MarkdownRenderChild} from "obsidian";
|
||||||
import { SimpleTimeTrackerSettings } from "./settings";
|
import {SimpleTimeTrackerSettings} from "./settings";
|
||||||
import { ConfirmModal } from "./confirm-modal";
|
import { ConfirmModal } from "./confirm-modal";
|
||||||
|
|
||||||
export interface Tracker {
|
export interface Tracker {
|
||||||
|
@ -10,11 +10,10 @@ export interface Entry {
|
||||||
name: string;
|
name: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
subEntries?: Entry[];
|
subEntries: Entry[];
|
||||||
collapsed?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTracker(tracker: Tracker, fileName: string, section: MarkdownSectionInformation): Promise<void> {
|
export async function saveTracker(tracker: Tracker, app: App, fileName: string, section: MarkdownSectionInformation): Promise<void> {
|
||||||
let file = app.vault.getAbstractFileByPath(fileName) as TFile;
|
let file = app.vault.getAbstractFileByPath(fileName) as TFile;
|
||||||
if (!file)
|
if (!file)
|
||||||
return;
|
return;
|
||||||
|
@ -34,43 +33,19 @@ export function loadTracker(json: string): Tracker {
|
||||||
if (json) {
|
if (json) {
|
||||||
try {
|
try {
|
||||||
let ret = JSON.parse(json);
|
let ret = JSON.parse(json);
|
||||||
updateLegacyInfo(ret.entries);
|
fixLegacyTimestamps(ret.entries);
|
||||||
return ret;
|
return ret;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Failed to parse Tracker from ${json}`);
|
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;
|
type GetFile = () => string;
|
||||||
|
|
||||||
export function displayTracker(tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
|
export function displayTracker(app: App, tracker: Tracker, element: HTMLElement, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, component: MarkdownRenderChild): void {
|
||||||
|
|
||||||
element.addClass("simple-time-tracker-container");
|
element.addClass("simple-time-tracker-container");
|
||||||
// add start/stop controls
|
// add start/stop controls
|
||||||
let running = isRunning(tracker);
|
let running = isRunning(tracker);
|
||||||
|
@ -84,7 +59,7 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
|
||||||
} else {
|
} else {
|
||||||
startNewEntry(tracker, newSegmentNameBox.getValue());
|
startNewEntry(tracker, newSegmentNameBox.getValue());
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
});
|
});
|
||||||
btn.buttonEl.addClass("simple-time-tracker-btn");
|
btn.buttonEl.addClass("simple-time-tracker-btn");
|
||||||
let newSegmentNameBox = new TextComponent(element)
|
let newSegmentNameBox = new TextComponent(element)
|
||||||
|
@ -93,29 +68,29 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
|
||||||
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
|
newSegmentNameBox.inputEl.addClass("simple-time-tracker-txt");
|
||||||
|
|
||||||
// add timers
|
// add timers
|
||||||
let timer = element.createDiv({ cls: "simple-time-tracker-timers" });
|
let timer = element.createDiv({cls: "simple-time-tracker-timers"});
|
||||||
let currentDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
let currentDiv = timer.createEl("div", {cls: "simple-time-tracker-timer"});
|
||||||
let current = currentDiv.createEl("span", { cls: "simple-time-tracker-timer-time" });
|
let current = currentDiv.createEl("span", {cls: "simple-time-tracker-timer-time"});
|
||||||
currentDiv.createEl("span", { text: "Current" });
|
currentDiv.createEl("span", {text: "Current"});
|
||||||
let totalDiv = timer.createEl("div", { cls: "simple-time-tracker-timer" });
|
let totalDiv = timer.createEl("div", {cls: "simple-time-tracker-timer"});
|
||||||
let total = totalDiv.createEl("span", { cls: "simple-time-tracker-timer-time", text: "0s" });
|
let total = totalDiv.createEl("span", {cls: "simple-time-tracker-timer-time", text: "0s"});
|
||||||
totalDiv.createEl("span", { text: "Total" });
|
totalDiv.createEl("span", {text: "Total"});
|
||||||
|
|
||||||
if (tracker.entries.length > 0) {
|
if (tracker.entries.length > 0) {
|
||||||
// add table
|
// 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(
|
table.createEl("tr").append(
|
||||||
createEl("th", { text: "Segment" }),
|
createEl("th", {text: "Segment"}),
|
||||||
createEl("th", { text: "Start time" }),
|
createEl("th", {text: "Start time"}),
|
||||||
createEl("th", { text: "End time" }),
|
createEl("th", {text: "End time"}),
|
||||||
createEl("th", { text: "Duration" }),
|
createEl("th", {text: "Duration"}),
|
||||||
createEl("th"));
|
createEl("th"));
|
||||||
|
|
||||||
for (let entry of orderedEntries(tracker.entries, settings))
|
for (let entry of orderedEntries(tracker.entries, settings))
|
||||||
addEditableTableRow(tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
|
addEditableTableRow(app, tracker, entry, table, newSegmentNameBox, running, getFile, getSectionInfo, settings, 0, component);
|
||||||
|
|
||||||
// add copy buttons
|
// add copy buttons
|
||||||
let buttons = element.createEl("div", { cls: "simple-time-tracker-bottom" });
|
let buttons = element.createEl("div", {cls: "simple-time-tracker-bottom"});
|
||||||
new ButtonComponent(buttons)
|
new ButtonComponent(buttons)
|
||||||
.setButtonText("Copy as table")
|
.setButtonText("Copy as table")
|
||||||
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
|
.onClick(() => navigator.clipboard.writeText(createMarkdownTable(tracker, settings)));
|
||||||
|
@ -136,27 +111,57 @@ export function displayTracker(tracker: Tracker, element: HTMLElement, getFile:
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDuration(entry: Entry): number {
|
function startSubEntry(entry: Entry, name: string): void {
|
||||||
if (entry.subEntries) {
|
// if this entry is not split yet, we add its time as a sub-entry instead
|
||||||
return getTotalDuration(entry.subEntries);
|
if (!entry.subEntries) {
|
||||||
} else {
|
entry.subEntries = [{...entry, name: `Part 1`}];
|
||||||
let endTime = entry.endTime ? moment(entry.endTime) : moment();
|
entry.startTime = null;
|
||||||
return endTime.diff(moment(entry.startTime));
|
entry.endTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!name)
|
||||||
|
name = `Part ${entry.subEntries.length + 1}`;
|
||||||
|
entry.subEntries.push({name: name, startTime: moment().toISOString(), endTime: null, subEntries: null});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTotalDuration(entries: Entry[]): number {
|
function startNewEntry(tracker: Tracker, name: string): void {
|
||||||
let ret = 0;
|
if (!name)
|
||||||
for (let entry of entries)
|
name = `Segment ${tracker.entries.length + 1}`;
|
||||||
ret += getDuration(entry);
|
let entry: Entry = {name: name, startTime: moment().toISOString(), endTime: null, subEntries: null};
|
||||||
return ret;
|
tracker.entries.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRunning(tracker: Tracker): boolean {
|
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 = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunning(tracker: Tracker): boolean {
|
||||||
return !!getRunningEntry(tracker.entries);
|
return !!getRunningEntry(tracker.entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRunningEntry(entries: Entry[]): Entry {
|
function getRunningEntry(entries: Entry[]): Entry {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
// if this entry has sub entries, check if one of them is running
|
// if this entry has sub entries, check if one of them is running
|
||||||
if (entry.subEntries) {
|
if (entry.subEntries) {
|
||||||
|
@ -172,46 +177,46 @@ export function getRunningEntry(entries: Entry[]): Entry {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
function getDuration(entry: Entry): number {
|
||||||
let table = [["Segment", "Start time", "End time", "Duration"]];
|
if (entry.subEntries) {
|
||||||
for (let entry of orderedEntries(tracker.entries, settings))
|
return getTotalDuration(entry.subEntries);
|
||||||
table.push(...createTableSection(entry, settings));
|
} else {
|
||||||
table.push(["**Total**", "", "", `**${formatDuration(getTotalDuration(tracker.entries), settings)}**`]);
|
let endTime = entry.endTime ? moment(entry.endTime) : moment();
|
||||||
|
return endTime.diff(moment(entry.startTime));
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalDuration(entries: Entry[]): number {
|
||||||
|
let ret = 0;
|
||||||
|
for (let entry of entries)
|
||||||
|
ret += getDuration(entry);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCsv(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
function setCountdownValues(tracker: Tracker, current: HTMLElement, total: HTMLElement, currentDiv: HTMLDivElement, settings: SimpleTimeTrackerSettings): void {
|
||||||
let ret = "";
|
let running = getRunningEntry(tracker.entries);
|
||||||
for (let entry of orderedEntries(tracker.entries, settings)) {
|
if (running && !running.endTime) {
|
||||||
for (let row of createTableSection(entry, settings))
|
current.setText(formatDuration(getDuration(running), settings));
|
||||||
ret += row.join(settings.csvDelimiter) + "\n";
|
currentDiv.hidden = false;
|
||||||
|
} else {
|
||||||
|
currentDiv.hidden = true;
|
||||||
}
|
}
|
||||||
return ret;
|
total.setText(formatDuration(getTotalDuration(tracker.entries), settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orderedEntries(entries: Entry[], settings: SimpleTimeTrackerSettings): Entry[] {
|
function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
return settings.reverseSegmentOrder ? entries.slice().reverse() : entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
|
||||||
return moment(timestamp).format(settings.timestampFormat);
|
return moment(timestamp).format(settings.timestampFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
|
function formatEditableTimestamp(timestamp: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
|
return moment(timestamp).format(settings.editableTimestampFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unformatEditableTimestamp(formatted: string, settings: SimpleTimeTrackerSettings): string {
|
||||||
|
return moment(formatted, settings.editableTimestampFormat).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(totalTime: number, settings: SimpleTimeTrackerSettings): string {
|
||||||
let ret = "";
|
let ret = "";
|
||||||
let duration = moment.duration(totalTime);
|
let duration = moment.duration(totalTime);
|
||||||
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
|
let hours = settings.fineGrainedDurations ? duration.hours() : Math.floor(duration.asHours());
|
||||||
|
@ -242,89 +247,48 @@ export function formatDuration(totalTime: number, settings: SimpleTimeTrackerSet
|
||||||
return ret;
|
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) {
|
for (let entry of entries) {
|
||||||
// in 0.1.8, timestamps were changed from unix to iso
|
|
||||||
if (entry.startTime && !isNaN(+entry.startTime))
|
if (entry.startTime && !isNaN(+entry.startTime))
|
||||||
entry.startTime = moment.unix(+entry.startTime).toISOString();
|
entry.startTime = moment.unix(+entry.startTime).toISOString();
|
||||||
if (entry.endTime && !isNaN(+entry.endTime))
|
if (entry.endTime && !isNaN(+entry.endTime))
|
||||||
entry.endTime = moment.unix(+entry.endTime).toISOString();
|
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)
|
if (entry.subEntries)
|
||||||
updateLegacyInfo(entry.subEntries);
|
fixLegacyTimestamps(entry.subEntries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMarkdownTable(tracker: Tracker, settings: SimpleTimeTrackerSettings): string {
|
||||||
|
let table = [["Segment", "Start time", "End time", "Duration"]];
|
||||||
|
for (let entry of orderedEntries(tracker.entries, settings))
|
||||||
|
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[][] {
|
function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings): string[][] {
|
||||||
let ret = [[
|
let ret = [[
|
||||||
|
@ -339,7 +303,11 @@ function createTableSection(entry: Entry, settings: SimpleTimeTrackerSettings):
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableElement, newSegmentNameBox: TextComponent, trackerRunning: boolean, getFile: GetFile, getSectionInfo: () => MarkdownSectionInformation, settings: SimpleTimeTrackerSettings, indent: number, component: MarkdownRenderChild): void {
|
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 {
|
||||||
let entryRunning = getRunningEntry(tracker.entries) == entry;
|
let entryRunning = getRunningEntry(tracker.entries) == entry;
|
||||||
let row = table.createEl("tr");
|
let row = table.createEl("tr");
|
||||||
|
|
||||||
|
@ -347,25 +315,10 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
let startField = new EditableTimestampField(row, (entry.startTime), settings);
|
let startField = new EditableTimestampField(row, (entry.startTime), settings);
|
||||||
let endField = new EditableTimestampField(row, (entry.endTime), 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);
|
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");
|
let entryButtons = row.createEl("td");
|
||||||
entryButtons.addClass("simple-time-tracker-table-buttons");
|
entryButtons.addClass("simple-time-tracker-table-buttons");
|
||||||
new ButtonComponent(entryButtons)
|
new ButtonComponent(entryButtons)
|
||||||
|
@ -375,7 +328,7 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
.setDisabled(trackerRunning)
|
.setDisabled(trackerRunning)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
startSubEntry(entry, newSegmentNameBox.getValue());
|
startSubEntry(entry, newSegmentNameBox.getValue());
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
});
|
});
|
||||||
let editButton = new ButtonComponent(entryButtons)
|
let editButton = new ButtonComponent(entryButtons)
|
||||||
.setClass("clickable-icon")
|
.setClass("clickable-icon")
|
||||||
|
@ -384,20 +337,18 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
if (nameField.editing()) {
|
if (nameField.editing()) {
|
||||||
entry.name = nameField.endEdit();
|
entry.name = nameField.endEdit();
|
||||||
expandButton.buttonEl.style.display = null;
|
|
||||||
startField.endEdit();
|
startField.endEdit();
|
||||||
entry.startTime = startField.getTimestamp();
|
entry.startTime = startField.getTimestamp();
|
||||||
if (!entryRunning) {
|
if (!entryRunning) {
|
||||||
endField.endEdit();
|
endField.endEdit();
|
||||||
entry.endTime = endField.getTimestamp();
|
entry.endTime = endField.getTimestamp();
|
||||||
}
|
}
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
editButton.setIcon("lucide-pencil");
|
editButton.setIcon("lucide-pencil");
|
||||||
|
|
||||||
renderNameAsMarkdown(nameField.label, getFile, component);
|
renderNameAsMarkdown(nameField.label, getFile, component);
|
||||||
} else {
|
} else {
|
||||||
nameField.beginEdit(entry.name);
|
nameField.beginEdit(entry.name);
|
||||||
expandButton.buttonEl.style.display = "none";
|
|
||||||
// only allow editing start and end times if we don't have sub entries
|
// only allow editing start and end times if we don't have sub entries
|
||||||
if (!entry.subEntries) {
|
if (!entry.subEntries) {
|
||||||
startField.beginEdit(entry.startTime);
|
startField.beginEdit(entry.startTime);
|
||||||
|
@ -414,23 +365,23 @@ function addEditableTableRow(tracker: Tracker, entry: Entry, table: HTMLTableEle
|
||||||
.setDisabled(entryRunning)
|
.setDisabled(entryRunning)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
|
|
||||||
const confirmed = await showConfirm("Are you sure you want to delete this entry?");
|
const confirmed = await showConfirm(app, "Are you sure you want to delete this entry?")
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEntry(tracker.entries, entry);
|
removeEntry(tracker.entries, entry);
|
||||||
await saveTracker(tracker, getFile(), getSectionInfo());
|
await saveTracker(tracker, this.app, getFile(), getSectionInfo());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (entry.subEntries && !entry.collapsed) {
|
if (entry.subEntries) {
|
||||||
for (let sub of orderedEntries(entry.subEntries, settings))
|
for (let sub of orderedEntries(entry.subEntries, settings))
|
||||||
addEditableTableRow(tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
|
addEditableTableRow(app, tracker, sub, table, newSegmentNameBox, trackerRunning, getFile, getSectionInfo, settings, indent + 1, component);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirm(message: string): Promise<boolean> {
|
function showConfirm(app: App, message: string): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const modal = new ConfirmModal(app, message, resolve);
|
const modal = new ConfirmModal(app, message, resolve);
|
||||||
modal.open();
|
modal.open();
|
||||||
|
@ -438,7 +389,6 @@ function showConfirm(message: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNameAsMarkdown(label: HTMLSpanElement, getFile: GetFile, component: Component): void {
|
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);
|
void MarkdownRenderer.renderMarkdown(label.innerHTML, label, getFile(), component);
|
||||||
// rendering wraps it in a paragraph
|
// rendering wraps it in a paragraph
|
||||||
label.innerHTML = label.querySelector("p").innerHTML;
|
label.innerHTML = label.querySelector("p").innerHTML;
|
||||||
|
@ -452,7 +402,7 @@ class EditableField {
|
||||||
|
|
||||||
constructor(row: HTMLTableRowElement, indent: number, value: string) {
|
constructor(row: HTMLTableRowElement, indent: number, value: string) {
|
||||||
this.cell = row.createEl("td");
|
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.label.style.marginLeft = `${indent}em`;
|
||||||
this.box = new TextComponent(this.cell).setValue(value);
|
this.box = new TextComponent(this.cell).setValue(value);
|
||||||
this.box.inputEl.addClass("simple-time-tracker-input");
|
this.box.inputEl.addClass("simple-time-tracker-input");
|
||||||
|
|
|
@ -64,12 +64,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-time-tracker-table .clickable-icon {
|
.simple-time-tracker-table .clickable-icon {
|
||||||
display: inline-block;
|
display: inline;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple-time-tracker-expand-button {
|
|
||||||
margin-inline-start: 0.5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-time-tracker-input {
|
.simple-time-tracker-input {
|
||||||
|
|
3
test-vault/.obsidian/community-plugins.json
vendored
3
test-vault/.obsidian/community-plugins.json
vendored
|
@ -1,4 +1,3 @@
|
||||||
[
|
[
|
||||||
"simple-time-tracker",
|
"simple-time-tracker"
|
||||||
"dataview"
|
|
||||||
]
|
]
|
50
test-vault/.obsidian/core-plugins.json
vendored
50
test-vault/.obsidian/core-plugins.json
vendored
|
@ -1,30 +1,20 @@
|
||||||
{
|
[
|
||||||
"file-explorer": true,
|
"file-explorer",
|
||||||
"global-search": true,
|
"global-search",
|
||||||
"switcher": true,
|
"switcher",
|
||||||
"graph": true,
|
"graph",
|
||||||
"backlink": true,
|
"backlink",
|
||||||
"outgoing-link": true,
|
"canvas",
|
||||||
"tag-pane": true,
|
"outgoing-link",
|
||||||
"page-preview": true,
|
"tag-pane",
|
||||||
"daily-notes": true,
|
"page-preview",
|
||||||
"templates": true,
|
"daily-notes",
|
||||||
"note-composer": true,
|
"templates",
|
||||||
"command-palette": true,
|
"note-composer",
|
||||||
"slash-command": false,
|
"command-palette",
|
||||||
"editor-status": true,
|
"editor-status",
|
||||||
"markdown-importer": false,
|
"bookmarks",
|
||||||
"zk-prefixer": false,
|
"outline",
|
||||||
"random-note": false,
|
"word-count",
|
||||||
"outline": true,
|
"file-recovery"
|
||||||
"word-count": true,
|
]
|
||||||
"slides": false,
|
|
||||||
"audio-recorder": false,
|
|
||||||
"workspaces": false,
|
|
||||||
"file-recovery": true,
|
|
||||||
"publish": false,
|
|
||||||
"sync": false,
|
|
||||||
"canvas": true,
|
|
||||||
"bookmarks": true,
|
|
||||||
"properties": false
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
!data.json
|
|
27
test-vault/.obsidian/plugins/dataview/data.json
vendored
27
test-vault/.obsidian/plugins/dataview/data.json
vendored
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
```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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,5 +1,4 @@
|
||||||
Tested for #tag, *italic*, [link](test2), etc:
|
Tested for #tag, *italic*, [link](test2), etc:
|
||||||
```simple-time-tracker
|
```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"},{"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"}]}
|
{"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}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,5 @@
|
||||||
"0.1.8": "1.3.0",
|
"0.1.8": "1.3.0",
|
||||||
"0.2.0": "1.3.0",
|
"0.2.0": "1.3.0",
|
||||||
"0.2.1": "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"
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue