diff --git a/manifest.json b/manifest.json index 46e819f..c99acdd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,11 @@ { "id": "timethings", "name": "Time Things", - "version": "1.3.0", + "version": "2.0.0", "minAppVersion": "0.15.0", - "description": "Show clock in the corner. Track total editing time of a note and the last time it was modified.", - "author": "Nick Winters", - "authorUrl": "https://github.com/plasmabit", - "fundingUrl": "https://www.patreon.com/nickwinters", + "description": "Track total editing time of a note and the last time it was modified. Show clock and/or editing indicator in the corner", + "author": "Nick Winters, Rupel Rupelsson", + "authorUrl": "https://github.com/plasmabit, https://github.com/rupel190", + "fundingUrl": "https://www.patreon.com/nickwinters, https://ko-fi.com/rupel190", "isDesktopOnly": true } diff --git a/src/CAMS.ts b/src/CAMS.ts index fbb07f6..d40b6d5 100644 --- a/src/CAMS.ts +++ b/src/CAMS.ts @@ -1,9 +1,5 @@ import { Editor } from "obsidian"; -export function isLineIndented(line: string): boolean { - return /^[\s\t]/.test(line); -} - export function getLine(editor: Editor, fieldPath: string): number | undefined { const frontmatterLine = frontmatterEndLine(editor); const keys = fieldPath.split("."); @@ -25,13 +21,10 @@ export function getLine(editor: Editor, fieldPath: string): number | undefined { if (currentFieldName === key) { emergingPath.push(currentFieldName); const targetPath = fieldPath.split("."); - const targetPathShrink = targetPath.slice( - 0, - emergingPath.length, - ); + const targetPathShrink = targetPath.slice(0, emergingPath.length); if ( - (targetPathShrink.join(".") === emergingPath.join(".")) === - false + (targetPathShrink.join(".") === emergingPath.join(".")) + === false ) { emergingPath.pop(); startLine = i + 1; @@ -63,10 +56,31 @@ export function getLine(editor: Editor, fieldPath: string): number | undefined { return undefined; } -export function isFrontmatterPresent(editor: Editor): boolean { +export function setLine(editor: Editor, fieldPath: string, fieldValue: string,) { + // Check for frontmatter + if(!isFrontmatterPresent(editor)) { + // Create empty frontmatter + editor.setLine(0, "---\n---\n"); + } + // Check for path + if(!fieldPathPresent(editor, fieldPath)) { + appendNewLine(editor, fieldPath, fieldValue); + } else { + overrideCurrentLine(editor, fieldPath, fieldValue); + } +} + +//#region private +function isLineIndented(line: string): boolean { + return /^[\s\t]/.test(line); +} + + +function isFrontmatterPresent(editor: Editor): boolean { if (editor.getLine(0) !== "---") { return false; } + for (let i = 1; i <= editor.lastLine(); i++) { if (editor.getLine(i) === "---") { return true; @@ -75,7 +89,7 @@ export function isFrontmatterPresent(editor: Editor): boolean { return false; } -export function frontmatterEndLine(editor: Editor): number | undefined { +function frontmatterEndLine(editor: Editor): number | undefined { if (isFrontmatterPresent(editor)) { for (let i = 1; i <= editor.lastLine(); i++) { if (editor.getLine(i) === "---") { @@ -86,14 +100,31 @@ export function frontmatterEndLine(editor: Editor): number | undefined { return undefined; // # End line not found } +function fieldPathPresent(editor: Editor, fieldPath: string): boolean { + const pathValue = getLine(editor, fieldPath) + if(pathValue) { + return true; + } + return false; +} -export function setValue(editor: Editor, fieldPath: string, fieldValue: string,) { - // The thing with this function is that it uses the format from settings to check against. I can make it as an argument that can be passed, or better yet, eradicate the check from the function to make it more atomic and place it somewhere else in the main code. - const fieldLine = getLine(editor, fieldPath); - if (fieldLine === undefined) { - return; - } - const initialLine = editor.getLine(fieldLine).split(":", 1); - const newLine = initialLine[0] + ": " + fieldValue; - editor.setLine(fieldLine, newLine); +function appendNewLine(editor: Editor, fieldPath: string, fieldValue: string) { + const endLine = frontmatterEndLine(editor); + if(!endLine) { + console.log("No frontmatter endline found!"); + return; + } + editor.setLine(endLine, fieldPath + ": " + fieldValue + "\n---"); +} + +function overrideCurrentLine(editor: Editor, fieldPath: string, fieldValue: string) { + const currentValue = getLine(editor, fieldPath); + if (currentValue === undefined) { + console.log("Value not found!"); + return; + } + const initialLine = editor.getLine(currentValue).split(":", 1); + const newLine = initialLine[0] + ": " + fieldValue; + editor.setLine(currentValue, newLine); } +//#endregion \ No newline at end of file diff --git a/src/gates.utils.ts b/src/gates.utils.ts index 29b7f14..a8d2f4d 100644 --- a/src/gates.utils.ts +++ b/src/gates.utils.ts @@ -11,7 +11,7 @@ export interface FilterList { export function isFileMatchFilter(file: TFile, filter: FilterList,): boolean { // Check if file matches paths - if (isStringInList(file.parent.path, filter.folders)) { + if (file.parent && isStringInList(file.parent.path, filter.folders)) { return true; } // Check if file matches tags @@ -27,7 +27,7 @@ export function isStringInList(path: string, list: string[]): boolean { export async function isTagPresentInFile(file: TFile, tag: string,) { await this.app.fileManager.processFrontMatter( file as TFile, - (frontmatter) => { + (frontmatter: any) => { const updateKeyValue = BOMS.getValue(frontmatter, "tags"); if (updateKeyValue.includes(tag)) { diff --git a/src/main.ts b/src/main.ts index 7b6d237..8769280 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,8 +4,9 @@ import { WorkspaceLeaf, Plugin, TFile, + debounce, + moment } from "obsidian"; -import { moment } from "obsidian"; import { MostEditedView, VIEW_TYPE_MOST_EDITED as VIEW_TYPE_MOST_EDITED, @@ -20,20 +21,35 @@ import { } from "./settings"; import * as timeUtils from "./time.utils"; import * as gates from "./gates.utils"; +import { allowedNodeEnvironmentFlags } from "process"; export default class TimeThings extends Plugin { settings: TimeThingsSettings; isDebugBuild: boolean; + + // Edit tracking + isEditing = false; // Not a lock but used for tracking (Status bar) + startTime: number | null; // How long was isEditing active + activityIconActive : boolean = false; // Will match the editing timer of isEditing, but it's better to decouple these variables + timeout: number; // Loaded from settings, timeout for tracking and periodic saving + + // Status bar clockBar: HTMLElement; // # Required + editIndicatorBar: HTMLElement; debugBar: HTMLElement; - editDurationBar: HTMLElement; - allowEditDurationUpdate: boolean; - isProccessing = false; - + + // Debounced functions + updateFrontmatter: (useCustomSolution: boolean, activeView: MarkdownView) => void; + resetEditing: () => void; + resetIcon: () => void; + + + //#region Load plugin async onload() { + // Load settings + await this.loadSettings(); // Add commands - this.addCommand( { id: 'Show most edited notes view', @@ -45,31 +61,24 @@ export default class TimeThings extends Plugin { ); // Add buttons - this.addRibbonIcon("history", "Activate view", () => { this.activateMostEditedNotesView(); }); // Register views - this.registerView( VIEW_TYPE_MOST_EDITED, (leaf) => new MostEditedView(leaf), ); - // Load settings - - await this.loadSettings(); - // Variables initialization - this.isDebugBuild = false; // for debugging purposes - this.allowEditDurationUpdate = true; // for cooldown + this.isDebugBuild = false; // for debugging purposes TODO: reset // Set up Status Bar items this.setUpStatusBarItems(); // Events initialization - this.registerFileModificationEvent(); + // this.registerFileModificationEvent(); this.registerKeyDownDOMEvent(); this.registerLeafChangeEvent(); this.registerMouseDownDOMEvent(); @@ -77,53 +86,37 @@ export default class TimeThings extends Plugin { // Add a tab for settings this.addSettingTab(new TimeThingsSettingsTab(this.app, this)); } + //#endregion - registerMouseDownDOMEvent() { - this.registerDomEvent(document, "mousedown", (evt: MouseEvent) => { - // Prepare everything - - const activeView = - this.app.workspace.getActiveViewOfType(MarkdownView); - if (activeView === null) { - return; - } - const editor: Editor = activeView.editor; - if (editor.hasFocus() === false) { - return; - } - - this.onUserActivity(true, activeView, { updateMetadata: false, updateStatusBar: true }); - }); - } + //#region UserActivity events + // CAMS registerLeafChangeEvent() { this.registerEvent( this.app.workspace.on("active-leaf-change", (leaf) => { // Prepare everything + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + const useCustom : boolean = this.settings.useCustomFrontmatterHandlingSolution; + this.isDebugBuild && console.log(`Key down, use: ${useCustom ? "CAMS" : "BOMS"}`); - const activeView = - this.app.workspace.getActiveViewOfType(MarkdownView); if (activeView === null) { return; } - const editor = activeView.editor; - if (editor.hasFocus() === false) { - return; - } - - // Change the duration icon in status bar - this.onUserActivity(true, activeView, { - updateMetadata: false, - updateStatusBar: true, - }); + if(useCustom) { + const editor = activeView.editor; + if (editor.hasFocus() === false) { + return; + } + } + this.onUserActivity(useCustom, activeView); }), ); } + // CAMS registerKeyDownDOMEvent() { this.registerDomEvent(document, "keyup", (evt: KeyboardEvent) => { - // If CAMS enabled const ignoreKeys = [ "ArrowDown", "ArrowUp", @@ -144,52 +137,66 @@ export default class TimeThings extends Plugin { return; } - if (this.settings.useCustomFrontmatterHandlingSolution === true) { - // Make sure the document is ready for edit + // Make sure the document is ready for edit + const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); + const useCustom : boolean = this.settings.useCustomFrontmatterHandlingSolution; + // this.isDebugBuild && console.log(`Key down, use: ${useCustom ? "CAMS" : "BOMS"}`); + + if (activeView === null) { + this.isDebugBuild && console.log("No active view"); + return; + } - const activeView = - this.app.workspace.getActiveViewOfType(MarkdownView); - if (activeView === null) { - if (this.isDebugBuild) { - console.log("No active view"); - } - return; - } + if (useCustom) { const editor: Editor = activeView.editor; if (editor.hasFocus() === false) { - if (this.isDebugBuild) { - console.log("No focus"); - } + this.isDebugBuild && console.log("No focus"); return; - } - - // Update everything - - this.onUserActivity(true, activeView); + } } + this.onUserActivity(useCustom, activeView); }); } - registerFileModificationEvent() { - this.registerEvent( - this.app.vault.on("modify", (file) => { - // Make everything ready for edit - - const activeView = - this.app.workspace.getActiveViewOfType(MarkdownView); - if (activeView === null) { - return; - } + // UNUSED + // BOMS + // registerFileModificationEvent() { + // // ! If BOMS is updated it triggers a new file modification event + // this.registerEvent( + // this.app.vault.on("modify", (file) => { + // // Make everything ready for edit + // const activeView = + // this.app.workspace.getActiveViewOfType(MarkdownView); + // if (activeView === null) { + // return; + // } + // console.log('filemod'); + + // if (this.settings.useCustomFrontmatterHandlingSolution === false) { + // this.onUserActivity(false, activeView); + // } + // }), + // ); + // } + + // UNUSED + registerMouseDownDOMEvent() { + this.registerDomEvent(document, "mousedown", (evt: MouseEvent) => { + // Prepare everything + const activeView = + this.app.workspace.getActiveViewOfType(MarkdownView); + if (activeView === null) { + return; + } + const editor: Editor = activeView.editor; + if (editor.hasFocus() === false) { + return; + } - // Main - if ( - this.settings.useCustomFrontmatterHandlingSolution === false - ) { - this.onUserActivity(false, activeView); - } - }), - ); + this.onUserActivity(true, activeView, { updateMetadata: false, updateTypingIndicator: false }); + }); } + // #endregion async activateMostEditedNotesView() { @@ -205,177 +212,181 @@ export default class TimeThings extends Plugin { // Our view could not be found in the workspace, create a new leaf // in the right sidebar for it leaf = workspace.getRightLeaf(false); - await leaf.setViewState({ + await leaf?.setViewState({ type: VIEW_TYPE_MOST_EDITED, active: true, }); } // "Reveal" the leaf in case it is in a collapsed sidebar - workspace.revealLeaf(leaf); + if(leaf) { + workspace.revealLeaf(leaf); + } + } + + + //region Editing tracking + updateEditing(useCustomSolution: boolean, activeView: MarkdownView) { + // Save current time only once, regardless of repeated calls (flag) + if(!this.isEditing) { + this.isEditing = true; + this.startTime = moment.now(); + this.isDebugBuild && console.log(`Editing ${this.isEditing} with startTime `, moment(this.startTime).format(this.settings.modifiedKeyFormat)); + } + this.updateFrontmatter(useCustomSolution, activeView); + this.resetEditing(); + } + + validEditDuration() : number | null { + const diffSeconds = (moment.now() - moment.duration(this.startTime).asMilliseconds()) / 1000; + return isNaN(diffSeconds) ? null : diffSeconds; + } + + updateMetadata (useCustomSolution: boolean, activeView: MarkdownView) { + let environment; + useCustomSolution ? environment = activeView.editor : environment = activeView.file; + const editDiff = this.validEditDuration() + const modificationThreshold = this.settings.modifiedThreshold/1000; + + if (useCustomSolution && environment instanceof Editor) { + // CAMS: Custom Asset Management System + this.isDebugBuild && console.log("Calling CAMS handler"); + if(editDiff !== null && editDiff >= modificationThreshold) { + this.isDebugBuild && console.log(`Modified property threshold reached with ${editDiff}s, update property!`) + this.updateModifiedPropertyCAMS(environment); + } + if (this.settings.enableEditDurationKey) { + this.updateDurationPropertyCAMS(environment); + } + } else if (!useCustomSolution && environment instanceof TFile) { + // BOMS: Build-in Object Management System + this.isDebugBuild && console.log("Calling BOMS handler"); + if(editDiff !== null && editDiff >= modificationThreshold) { + this.isDebugBuild && console.log(`Modified property threshold reached with ${editDiff}s, update property!`) + this.updateModifiedPropertyBOMS(environment); + } + if (this.settings.enableEditDurationKey) { + this.updateDurationPropertyBOMS(environment); + } + } } - // A function for reading and editing metadata realtime + // Called on typing onUserActivity( useCustomSolution: boolean, activeView: MarkdownView, - options: { updateMetadata: boolean, updateStatusBar: boolean, } = { updateMetadata: true, updateStatusBar: true, }, + options: { updateMetadata: boolean, updateTypingIndicator: boolean, } = { updateMetadata: true, updateTypingIndicator: true, }, ) { - const { updateMetadata, updateStatusBar } = options; - // Gets called when a user changes a leaf, clicks a mouse, types in the editor, or modifies a file - let environment; + const { updateMetadata, updateTypingIndicator } = options; + let environment; useCustomSolution ? environment = activeView.editor : environment = activeView.file; - // Check if the file is in the blacklisted folder // Check if the file has a property that puts it into a blacklist // Check if the file itself is in the blacklist - - // - if (updateStatusBar) { - // update status bar - } - - // Update metadata using either BOMS or cams + if (updateMetadata) { - if ( - useCustomSolution && - environment instanceof Editor - ) { - // CAMS - this.updateModifiedPropertyEditor(environment); - if (this.settings.enableEditDurationKey) { - this.updateDurationPropertyEditor(environment); - } - } else if ( - !useCustomSolution && - environment instanceof TFile - ) { - // BOMS - this.updateModifiedPropertyFrontmatter(environment); - if (this.settings.enableEditDurationKey) { - this.updateDurationPropertyFrontmatter(environment); - } + // Update metadata using either BOMS or CAMS + // this.isDebugBuild && console.log(`UserActivity: ${useCustomSolution ? "CAMS" : "BOMS"}, with timeout ${this.timeout}`); + if(updateTypingIndicator) { + this.updateIcon(); } + this.updateEditing(useCustomSolution, activeView); } } + //#endregion - updateModifiedPropertyEditor(editor: Editor) { - const dateNow = moment(); - const userDateFormat = this.settings.modifiedKeyFormat; - const dateFormatted = dateNow.format(userDateFormat); + //#region Frontmatter update modified + // CAMS + updateModifiedPropertyCAMS(editor: Editor) { + this.isDebugBuild && console.log('*** CAMS: Update modified property! ***'); + // With the old solution updating frontmatter keys only worked on BOMS! + const userDateFormat = this.settings.modifiedKeyFormat; // Target format. Existing format unknown and irrelevant. const userModifiedKeyName = this.settings.modifiedKeyName; - const valueLineNumber = CAMS.getLine(editor, userModifiedKeyName); + const dateFormatted = moment().format(userDateFormat); + CAMS.setLine(editor, userModifiedKeyName, dateFormatted); + } - if (typeof valueLineNumber !== "number") { - this.isDebugBuild && console.log("Couldn't get the line number of last_modified property"); - return; - } - const value = editor.getLine(valueLineNumber).split(/:(.*)/s)[1].trim(); - if (moment(value, userDateFormat, true).isValid() === false) { - // Little safecheck in place to reduce chance of bugs - this.isDebugBuild && console.log("Wrong format of last_modified property"); - return; - } - // this.setValue(true, editor, userModifiedKeyName, dateFormatted,); - CAMS.setValue(editor, userModifiedKeyName, dateFormatted); - } - - async updateModifiedPropertyFrontmatter(file: TFile) { + // BOMS (Default) + async updateModifiedPropertyBOMS(file: TFile) { + this.isDebugBuild && console.log('*** BOMS: Update modified property! ***'); await this.app.fileManager.processFrontMatter( file as TFile, (frontmatter) => { - const dateNow = moment(); - const dateFormatted = dateNow.format( - this.settings.modifiedKeyFormat, - ); - - const updateKeyValue = moment( - BOMS.getValue(frontmatter, this.settings.modifiedKeyName), - this.settings.modifiedKeyFormat, - ); - - if ( - updateKeyValue.add( - this.settings.updateIntervalFrontmatterMinutes, - "minutes", - ) > dateNow - ) { - return; - } - - BOMS.setValue( - frontmatter, - this.settings.modifiedKeyName, - dateFormatted, - ); + const dateFormatted = moment().format(this.settings.modifiedKeyFormat); + // BOMS creates key if it doesn't exist + BOMS.setValue(frontmatter, this.settings.modifiedKeyName, dateFormatted); }, ); } - - async updateDurationPropertyFrontmatter(file: TFile) { - // Prepare everything - if (this.allowEditDurationUpdate === false) { - return; - } - this.allowEditDurationUpdate = false; + //#region Frontmatter update duration + + + // CAMS + async updateDurationPropertyCAMS(editor: Editor) { + this.isDebugBuild && console.log('*** CAMS: Update duration property! ***'); + // With the old solution updating frontmatter keys only worked on BOMS! + // Fetch duration + const fieldLine: number | undefined = CAMS.getLine(editor, this.settings.editDurationKeyName); + const userDateFormat = this.settings.editDurationKeyFormat; + let newValue : any; + // Check for existing + if(fieldLine === undefined) { + newValue = moment.duration(0, "minutes").format(userDateFormat, { trim: false }) + } else { + newValue = editor.getLine(fieldLine).split(/:(.*)/s)[1].trim(); + } + // Increment & set + const incremented = moment.duration(newValue) + .add(this.timeout, 'milliseconds') + .format(userDateFormat, { trim: false }); // Force formatting + this.isDebugBuild && console.log(`Increment CAMS edit duration from ${newValue} to ${incremented} with formatter ${userDateFormat}`); + CAMS.setLine(editor, this.settings.editDurationKeyName, incremented.toString()); + } + + // BOMS (Default) + /* Date updating is delicate: Moment.js validity check might check an updated formatter + against a pre-existing date and would return false. So it would never act after format changes. + Instead: Check existing duration for general validity. Increment. Display in the given, pre-validated format. + */ + async updateDurationPropertyBOMS(file: TFile) { + this.isDebugBuild && console.log('*** BOMS: Update duration property! ***'); + // Slow update await this.app.fileManager.processFrontMatter( file as TFile, - (frontmatter) => { - let value = BOMS.getValue( - frontmatter, - this.settings.editDurationPath, - ); + (frontmatter: any) => { + // Fetch + let value = BOMS.getValue(frontmatter, this.settings.editDurationKeyName); + // Zero if non-existent if (value === undefined) { - value = "0"; + this.isDebugBuild && console.log('No edit duration, initialize with 0.'); + value = moment.duration(0); } - - // Increment - - const newValue = +value + 10; + // Check for general validity + if(!moment.duration(value).isValid()) { + console.log(`Unable to update ${this.settings.editDurationKeyName} due to invalid value of ${value}.`); + return; + } + // Increment + const userDateFormat = this.settings.editDurationKeyFormat; + const incremented = moment + .duration(value) + .add(this.timeout, 'milliseconds') + .format(userDateFormat, {trim: false}); + this.isDebugBuild && console.log(`Increment BOMS from ${value} to ${incremented}`); BOMS.setValue( frontmatter, - this.settings.editDurationPath, - newValue, + this.settings.editDurationKeyName, + incremented, ); }, ); - - // Cool down - - await sleep(10000 - this.settings.nonTypingEditingTimePercentage * 100); - this.allowEditDurationUpdate = true; } + //#endregion - async updateDurationPropertyEditor(editor: Editor) { - // Prepare everything - if (this.allowEditDurationUpdate === false) { - return; - } - this.allowEditDurationUpdate = false; - const fieldLine = CAMS.getLine(editor, this.settings.editDurationPath); - if (fieldLine === undefined) { - this.allowEditDurationUpdate = true; - return; - } - - // Increment - - const value = editor.getLine(fieldLine).split(/:(.*)/s)[1].trim(); - const newValue = +value + 1; - CAMS.setValue( - editor, - this.settings.editDurationPath, - newValue.toString(), - ); - - // Cool down - - await sleep(1000 - this.settings.nonTypingEditingTimePercentage * 10); - this.allowEditDurationUpdate = true; - } + //#region Status bar // Don't worry about it updateClockBar() { const dateNow = moment(); @@ -384,46 +395,95 @@ export default class TimeThings extends Plugin { const dateChosen = this.settings.isUTC ? dateUTC : dateNow; const dateFormatted = dateChosen.format(this.settings.clockFormat); const emoji = timeUtils.momentToClockEmoji(dateChosen); + + this.clockBar.setText(emoji + " " + dateFormatted) + // this.settings.enableClock + // ? this.clockBar.setText(emoji + " " + dateFormatted) + // : this.clockBar.setText(dateFormatted); + } - this.settings.showEmojiStatusBar - ? this.clockBar.setText(emoji + " " + dateFormatted) - : this.clockBar.setText(dateFormatted); + // Typing indicator + updateIcon() { + if(!this.activityIconActive) { + this.editIndicatorBar.setText(this.settings.editIndicatorActive); + this.activityIconActive = true; + this.isDebugBuild && console.log('Activate typing icon, active: ', this.activityIconActive, this.settings.editIndicatorActive); + } + this.resetIcon(); } - // Gets called on OnLoad + // Called on OnLoad, adds status bar setUpStatusBarItems() { + // Clock if (this.settings.enableClock) { - // Add clock icon - // Adds a status bar this.clockBar = this.addStatusBarItem(); - this.clockBar.setText(":)"); + this.clockBar.setText(timeUtils.momentToClockEmoji(moment())); // Change status bar text every second this.updateClockBar(); this.registerInterval( window.setInterval( this.updateClockBar.bind(this), - +this.settings.updateIntervalMilliseconds, + + this.timeout, ), ); } - + // Typing indicator + if (this.settings.enableEditIndicator) { + this.editIndicatorBar = this.addStatusBarItem(); + this.editIndicatorBar.setText(this.settings.editIndicatorInactive); + } } + //#endregion + // Don't worry about it onunload() {} - // Don't worry about it + async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, await this.loadData(), ); + + this.timeout = this.settings?.typingTimeoutMilliseconds; + if(!this.timeout || isNaN(this.timeout) || this.timeout === undefined) { + this.isDebugBuild && console.log(`Timeout setting ${this.timeout} invalid, fallback!`); + this.timeout = 10000; + } + + this.isDebugBuild && console.log("LOAD settings: ", this.timeout); + // Because the methods are stored in a variable, the values inside the closure will be stale. + // Reloading here keeps it fresh and decoupled from the settings file. + this.updateFrontmatter = debounce((useCustomSolution: boolean, activeView: MarkdownView) => { + if(this.startTime) { + this.isDebugBuild && console.log(`Update frontmatter using ${useCustomSolution ? "CAMS" : "BOMS"}`); + this.updateMetadata(useCustomSolution, activeView); + } + }, this.timeout); + + this.resetIcon = debounce(() => { + // Inactive typing + this.editIndicatorBar.setText(this.settings.editIndicatorInactive); + this.activityIconActive = false; + this.isDebugBuild && console.log('Deactivate typing icon, active: ', this.activityIconActive, this.settings.editIndicatorInactive); + }, this.timeout, true); + + this.resetEditing = debounce(() => { + // Reset state + let diff: number = moment.now() - moment.duration(this.startTime).asMilliseconds(); + this.isDebugBuild && console.log(`Editing halted after ${diff/1000}s.`); + this.isEditing = false; + this.startTime = null; + }, this.timeout, true); } // Don't worry about it async saveSettings() { + this.isDebugBuild && console.log("SAVE settings") await this.saveData(this.settings); + await this.loadSettings(); } } diff --git a/src/settings.ts b/src/settings.ts index fdc232d..c2c5b6f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,65 +1,55 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, SliderComponent, TextComponent } from "obsidian"; import TimeThings from "./main"; +import moment from "moment"; export interface TimeThingsSettings { - //CAMS + //CAMS/BOMS useCustomFrontmatterHandlingSolution: boolean; - - //EMOJIS - showEmojiStatusBar: boolean; + typingTimeoutMilliseconds: number; //CLOCK clockFormat: string; - updateIntervalMilliseconds: string; enableClock: boolean; isUTC: boolean; - + //MODIFIED KEY + enableModifiedKey: boolean; modifiedKeyName: string; modifiedKeyFormat: string; - enableModifiedKeyUpdate: boolean; - //BOMS - updateIntervalFrontmatterMinutes: number; - + modifiedThreshold: number; + //DURATION KEY - editDurationPath: string; enableEditDurationKey: boolean; - nonTypingEditingTimePercentage: number; - - enableSwitch: boolean; - switchKey: string; - switchKeyValue: string; - - - + editDurationKeyName: string; + editDurationKeyFormat: string; + + // EDIT INDICATOR + enableEditIndicator: boolean; + editIndicatorActive: string; + editIndicatorInactive: string; } export const DEFAULT_SETTINGS: TimeThingsSettings = { useCustomFrontmatterHandlingSolution: false, - - showEmojiStatusBar: true, + typingTimeoutMilliseconds: 10000, // Default setting is BOMS, which triggers an endless loop due to the file modification event when going <10s clockFormat: "hh:mm A", - updateIntervalMilliseconds: "1000", enableClock: true, isUTC: false, - + modifiedKeyName: "updated_at", modifiedKeyFormat: "YYYY-MM-DD[T]HH:mm:ss.SSSZ", - enableModifiedKeyUpdate: true, - - editDurationPath: "edited_seconds", + enableModifiedKey: true, + modifiedThreshold: 30000, + + editDurationKeyName: "edited_seconds", + editDurationKeyFormat: "HH:mm:ss", enableEditDurationKey: true, - - updateIntervalFrontmatterMinutes: 1, - - nonTypingEditingTimePercentage: 22, - - enableSwitch: false, - switchKey: "timethings.switch", - switchKeyValue: "true", - - + + // EDIT INDICATOR + enableEditIndicator: true, + editIndicatorActive: "✏🔵", + editIndicatorInactive: "✋🔴", }; export class TimeThingsSettingsTab extends PluginSettingTab { @@ -74,8 +64,7 @@ export class TimeThingsSettingsTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - // #region prerequisites - + // #region Prerequisites const createLink = () => { const linkEl = document.createDocumentFragment(); @@ -87,54 +76,89 @@ export class TimeThingsSettingsTab extends PluginSettingTab { ); return linkEl; }; - // #endregion - // #region custom frontmatter solution + + + // #region General + let mySlider : SliderComponent; + let myText: TextComponent; + const minTimeoutBoms = 10; // Not sure + const minTimeoutCams = 1; new Setting(containerEl) .setName("Use custom frontmatter handling solution") - .setDesc( - "Smoother experience. Prone to bugs if you use a nested value.", - ) + .setDesc("Smoother experience. Prone to bugs if you use a nested value.",) .addToggle((toggle) => toggle - .setValue( - this.plugin.settings - .useCustomFrontmatterHandlingSolution, - ) + .setValue(this.plugin.settings.useCustomFrontmatterHandlingSolution,) .onChange(async (newValue) => { - this.plugin.settings.useCustomFrontmatterHandlingSolution = - newValue; + // console.log("Use custom frontmatter handling: ", newValue); + this.plugin.settings.useCustomFrontmatterHandlingSolution = newValue; + // await this.display(); // UI update obsolete + + if (this.plugin.settings.useCustomFrontmatterHandlingSolution) { + // CAMS: Reset lower limit + mySlider.setLimits(minTimeoutCams, 90, 1); + } + else { + // BOMS: Raise lower limit and bump if below + // console.log("Slider lower limit: ", mySlider.getValue()); + mySlider.setLimits(minTimeoutBoms, 90, 1); + if(this.plugin.settings.typingTimeoutMilliseconds < minTimeoutBoms * 1000) { + this.plugin.settings.typingTimeoutMilliseconds = minTimeoutBoms * 1000; + myText.setValue(minTimeoutBoms.toString()); + // console.log("Bump BOMS timeout", this.plugin.settings.typingTimeoutMilliseconds); + } + } await this.plugin.saveSettings(); - await this.display(); }), ); + new Setting(containerEl.createDiv({cls: "textbox"})) + .setName("Editing Timeout") + .setDesc("In seconds. Time to stop tracking after interaction has stopped. Value also used for saving interval. Textbox allows for higher values.") + .addSlider((slider) => mySlider = slider // implicit return without curlies + .setLimits(minTimeoutBoms, 90, 1) + .setValue(this.plugin.settings.typingTimeoutMilliseconds / 1000) + .onChange(async (value) => { + myText.setValue(value.toString()); + // Validity check including BOMS limit + const useCustom = this.plugin.settings.useCustomFrontmatterHandlingSolution; + if( + value < (useCustom? minTimeoutCams : minTimeoutBoms) + || isNaN(value) + ) { + myText.inputEl.addClass('invalid-format'); + } else { + myText.inputEl.removeClass('invalid-format'); + this.plugin.settings.typingTimeoutMilliseconds = value * 1000; + await this.plugin.saveSettings(); + } + }) + .setDynamicTooltip(), + ) + .addText((text) => { + myText = text + .setPlaceholder("50") + .setValue((this.plugin.settings.typingTimeoutMilliseconds/1000).toString(),) + .onChange(async (value) => { + const numericValue = parseInt(value, 10); + this.plugin.settings.typingTimeoutMilliseconds = numericValue * 1000; + mySlider.setValue(numericValue); + await this.plugin.saveSettings(); + }) + }); // #endregion - // #region status bar + // #region Status bar containerEl.createEl("h1", { text: "Status bar" }); - containerEl.createEl("p", { - text: "Displays clock in the status bar", - }); + containerEl.createEl("p", { text: "Display symbols in the status bar" }); containerEl.createEl("h2", { text: "🕰️ Clock" }); - new Setting(containerEl) - .setName("Enable emojis") - .setDesc("Show emojis in the status bar?") - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.showEmojiStatusBar) - .onChange(async (newValue) => { - this.plugin.settings.showEmojiStatusBar = newValue; - await this.plugin.saveSettings(); - await this.display(); - }), - ); new Setting(containerEl) - .setName("Enable status bar clock") + .setName("Enable clock") .setDesc( "Show clock on the status bar? This setting requires restart of the plugin.", ) @@ -144,6 +168,7 @@ export class TimeThingsSettingsTab extends PluginSettingTab { .onChange(async (newValue) => { this.plugin.settings.enableClock = newValue; await this.plugin.saveSettings(); + await this.plugin.loadSettings(); await this.display(); }), ); @@ -156,27 +181,17 @@ export class TimeThingsSettingsTab extends PluginSettingTab { text .setPlaceholder("hh:mm A") .setValue(this.plugin.settings.clockFormat) - .onChange(async (value) => { - this.plugin.settings.clockFormat = value; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Update interval") - .setDesc( - "In milliseconds. Restart plugin for this setting to take effect.", - ) - .addText((text) => - text - .setPlaceholder("1000") - .setValue( - this.plugin.settings.updateIntervalMilliseconds, - ) - .onChange(async (value) => { - this.plugin.settings.updateIntervalMilliseconds = - value; - await this.plugin.saveSettings(); + .onChange(async (formatter) => { + // Validate formatter by using it + const formatTest = moment().format(formatter); + const valid = moment(formatTest, formatter).isValid(); + if(!valid) { + text.inputEl.addClass('invalid-format'); + } else { + text.inputEl.removeClass('invalid-format'); + this.plugin.settings.clockFormat = formatter; + await this.plugin.saveSettings(); + } }), ); @@ -193,33 +208,69 @@ export class TimeThingsSettingsTab extends PluginSettingTab { ); } + containerEl.createEl("h2", { text: "✏ Typing indicator" }); + new Setting(containerEl) + .setName("Enable typing indicator") + .setDesc("Show typing indicator in the status bar? This setting requires restart of the plugin.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableEditIndicator) + .onChange(async (newValue) => { + this.plugin.settings.enableEditIndicator = newValue; + await this.plugin.saveSettings(); + await this.display(); + }), + ); + + if (this.plugin.settings.enableEditIndicator === true) { + new Setting(containerEl.createDiv({cls: "statusBarTypingIndicator"})) + .setName("Icon for tracking active/inactive") + .addText((text) => + text + .setPlaceholder("Active") + .setValue(this.plugin.settings.editIndicatorActive) + .onChange(async (value) => { + // console.log('update active tracking icon: ', value) + this.plugin.settings.editIndicatorActive = value; + await this.plugin.saveSettings(); + }), + ) + .addText((text) => + text + .setPlaceholder("Inactive") + .setValue(this.plugin.settings.editIndicatorInactive) + .onChange(async (value) => { + // console.log('update inactive tracking icon: ', value) + this.plugin.settings.editIndicatorInactive = value; + await this.plugin.saveSettings(); + }), + ); + } // #endregion - // #region keys + // #region Frontmatter containerEl.createEl("h1", { text: "Frontmatter" }); - containerEl.createEl("p", { - text: "Handles timestamp keys in frontmatter.", - }); - - // #region updated_at key + containerEl.createEl("p", { text: "Handles timestamp keys in frontmatter." }); + // Modified timestamp containerEl.createEl("h2", { text: "🔑 Modified timestamp" }); - + containerEl.createEl("p", { text: "Track the last time a note was edited." }); + new Setting(containerEl) .setName("Enable update of the modified key") .setDesc("") .addToggle((toggle) => toggle - .setValue(this.plugin.settings.enableModifiedKeyUpdate) + .setValue(this.plugin.settings.enableModifiedKey) .onChange(async (newValue) => { - this.plugin.settings.enableModifiedKeyUpdate = newValue; + this.plugin.settings.enableModifiedKey = newValue; await this.plugin.saveSettings(); await this.display(); }), ); - if (this.plugin.settings.enableModifiedKeyUpdate === true) { + if (this.plugin.settings.enableModifiedKey === true) { new Setting(containerEl) .setName("Modified key name") .setDesc( @@ -242,40 +293,49 @@ export class TimeThingsSettingsTab extends PluginSettingTab { text .setPlaceholder("YYYY-MM-DD[T]HH:mm:ss.SSSZ") .setValue(this.plugin.settings.modifiedKeyFormat) - .onChange(async (value) => { - this.plugin.settings.modifiedKeyFormat = value; - await this.plugin.saveSettings(); + .onChange(async (formatter) => { + // Validate formatter by using it + const formatTest = moment().format(formatter); + const valid = moment(formatTest, formatter).isValid(); + if(!valid) { + text.inputEl.addClass('invalid-format'); + } else { + text.inputEl.removeClass('invalid-format'); + this.plugin.settings.modifiedKeyFormat = formatter; + await this.plugin.saveSettings(); + } }), - ); + ) - if ( - this.plugin.settings.useCustomFrontmatterHandlingSolution === - false - ) { - new Setting(containerEl) - .setName("Interval between updates") - .setDesc("Only for Obsidian frontmatter API.") - .addSlider((slider) => - slider - .setLimits(1, 15, 1) - .setValue( - this.plugin.settings - .updateIntervalFrontmatterMinutes, - ) - .onChange(async (value) => { - this.plugin.settings.updateIntervalFrontmatterMinutes = - value; - await this.plugin.saveSettings(); - }) - .setDynamicTooltip(), - ); - } + let thresholdText: TextComponent; + new Setting(containerEl.createDiv({cls: "textbox"})) + .setName("Date refresh threshold") + .setDesc("Active typing duration that must be exceeded in one continuous period for the modification date to be updated.") + .addSlider((slider) => slider // implicit return without curlies + .setLimits(0, 60, 1) + .setValue(this.plugin.settings.modifiedThreshold / 1000) + .onChange(async (value) => { + this.plugin.settings.modifiedThreshold = value * 1000; + thresholdText.setValue(value.toString()); + await this.plugin.saveSettings(); + }) + .setDynamicTooltip(), + ) + .addText((text) => { + thresholdText = text + .setPlaceholder("30") + .setValue((this.plugin.settings.modifiedThreshold/1000).toString(),) + .onChange(async (value) => { + const numericValue = parseInt(value, 10); + this.plugin.settings.modifiedThreshold = numericValue * 1000; + mySlider.setValue(numericValue); + await this.plugin.saveSettings(); + }) + }); } - // #endregion - // #region edited_duration key - + // Edit duration containerEl.createEl("h2", { text: "🔑 Edited duration" }); containerEl.createEl("p", { text: "Track for how long you have been editing a note.", @@ -304,46 +364,38 @@ export class TimeThingsSettingsTab extends PluginSettingTab { .addText((text) => text .setPlaceholder("edited_seconds") - .setValue(this.plugin.settings.editDurationPath) + .setValue(this.plugin.settings.editDurationKeyName) .onChange(async (value) => { - this.plugin.settings.editDurationPath = value; + this.plugin.settings.editDurationKeyName = value; await this.plugin.saveSettings(); }), ); - const descA = document.createDocumentFragment(); - descA.append( - "The portion of time you are not typing when editing a note. Works best with custom frontmatter handling solution. ", - createEl("a", { - href: "https://github.com/DynamicPlayerSector/timethings/wiki/Calculating-your-non%E2%80%90typing-editing-percentage", - text: "How to calculate yours?", - }), - ); - new Setting(containerEl) - .setName("Non-typing editing time percentage") - .setDesc(descA) - .addSlider((slider) => - slider - .setLimits(0, 40, 2) - .setValue( - this.plugin.settings.nonTypingEditingTimePercentage, - ) - .onChange(async (value) => { - this.plugin.settings.nonTypingEditingTimePercentage = - value; - await this.plugin.saveSettings(); - }) - .setDynamicTooltip(), + .setName("Edited duration key format") + .setDesc(createLink()) + .addText((text) => + text + .setPlaceholder("HH:mm:ss.SSSZ") + .setValue(this.plugin.settings.editDurationKeyFormat) + .onChange(async (formatter) => { + // Validate formatter by using it + const formatTest = moment().format(formatter); + const valid = moment(formatTest, formatter).isValid(); + if(!valid) { + text.inputEl.addClass('invalid-format'); + } else { + text.inputEl.removeClass('invalid-format'); + this.plugin.settings.editDurationKeyFormat = formatter; + await this.plugin.saveSettings(); + } + }), ); - } - - // #endregion - + } // #endregion - // #region danger zone + // #region Danger zone containerEl.createEl("h1", { text: "Danger zone" }); containerEl.createEl("p", { text: "You've been warned!" }); diff --git a/styles.css b/styles.css index de1462c..f4e753a 100644 --- a/styles.css +++ b/styles.css @@ -24,4 +24,23 @@ If your plugin does not need CSS, delete this file. width: 100px; max-height: 1rem; text-align: end; -} \ No newline at end of file +} + +.statusBarTypingIndicator input[type="text"] +{ + display: flex; + justify-content: flex-end; + text-align: center; + width: 5rem; +} + +.textbox input[type="text"] +{ + text-align: center; + width: 3rem; +} + +.setting-item .setting-item-control .invalid-format { + border-color: orangered; +} +