From 6d6759bda1a6d21913db0ebd5e361fbea3126223 Mon Sep 17 00:00:00 2001 From: stfrigerio Date: Thu, 10 Apr 2025 17:37:14 +0200 Subject: [PATCH] timestamp button --- main.ts | 3 + .../createTimestampButtonElement.ts | 35 ++++++++ .../registerTimestampUpdaterButton.ts | 44 ++++++++++ .../timestampFileUpdater.ts | 88 +++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 src/webcomponents/TimestampUpdaterButton/createTimestampButtonElement.ts create mode 100644 src/webcomponents/TimestampUpdaterButton/registerTimestampUpdaterButton.ts create mode 100644 src/webcomponents/TimestampUpdaterButton/timestampFileUpdater.ts diff --git a/main.ts b/main.ts index a79cd83..99541c6 100644 --- a/main.ts +++ b/main.ts @@ -22,6 +22,7 @@ import { registerTextInput } from "src/webcomponents/TextInput/registerTextInput import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types"; import { pluginState } from "src/pluginState"; +import { registerTimestampUpdaterButton } from "src/webcomponents/TimestampUpdaterButton/registerTimestampUpdaterButton"; export default class SQLiteDBPlugin extends Plugin { settings: SQLiteDBSettings; @@ -45,6 +46,8 @@ export default class SQLiteDBPlugin extends Plugin { registerTextInput(el, this.app, this.dbService); }); + registerTimestampUpdaterButton(this); + //? Codeblocks this.registerMarkdownCodeBlockProcessor( "sql", diff --git a/src/webcomponents/TimestampUpdaterButton/createTimestampButtonElement.ts b/src/webcomponents/TimestampUpdaterButton/createTimestampButtonElement.ts new file mode 100644 index 0000000..59a0240 --- /dev/null +++ b/src/webcomponents/TimestampUpdaterButton/createTimestampButtonElement.ts @@ -0,0 +1,35 @@ +/** + * Creates the HTMLButtonElement for the timestamp updater. + * + * @param onClick The async function to execute when the button is clicked. + * @returns The created HTMLButtonElement. + */ +export function createTimestampButtonElement(onClick: () => Promise): HTMLButtonElement { + const button = document.createElement('button'); + button.setText('Update Timestamp'); + button.addClass('timestamp-updater-button'); + + button.style.backgroundColor = 'var(--interactive-accent)'; + button.style.color = 'var(--text-on-accent)'; + button.style.border = 'none'; + button.style.padding = '6px 12px'; + button.style.borderRadius = '4px'; + button.style.cursor = 'pointer'; + button.style.fontSize = '0.9em'; + + button.addEventListener('click', async (event) => { + event.stopPropagation(); + button.disabled = true; + button.style.opacity = '0.7'; + button.style.cursor = 'wait'; + try { + await onClick(); + } finally { + button.disabled = false; + button.style.opacity = '1'; + button.style.cursor = 'pointer'; + } + }); + + return button; +} \ No newline at end of file diff --git a/src/webcomponents/TimestampUpdaterButton/registerTimestampUpdaterButton.ts b/src/webcomponents/TimestampUpdaterButton/registerTimestampUpdaterButton.ts new file mode 100644 index 0000000..eba433e --- /dev/null +++ b/src/webcomponents/TimestampUpdaterButton/registerTimestampUpdaterButton.ts @@ -0,0 +1,44 @@ +import { App, MarkdownPostProcessorContext, Notice, Plugin, TFile } from 'obsidian'; +import { updateTimestampInFile } from './timestampFileUpdater'; +import { createTimestampButtonElement } from './createTimestampButtonElement'; + +/** + * Registers the Markdown postprocessor to find + * tags and replace them with functional buttons. + * + * @param plugin The Obsidian Plugin instance. + */ +export function registerTimestampUpdaterButton(plugin: Plugin): void { + + plugin.registerMarkdownPostProcessor((element, context: MarkdownPostProcessorContext) => { + const { app } = plugin; + console.log(`Timestamp PostProcessor running for section in: ${context.sourcePath}`); + + const placeholderElements = element.querySelectorAll('.timestamp-updater-placeholder'); + + if (placeholderElements.length === 0) { + return; + } + + placeholderElements.forEach((placeholderEl) => { + const handleClick = async () => { + const activeFile = app.workspace.getActiveFile(); + + if (!activeFile || !(activeFile instanceof TFile)) { + new Notice('No active file selected.', 3000); + return; + } + + const result = await updateTimestampInFile(app, activeFile); + new Notice(result.message, result.success ? 2000 : 5000); + }; + + const buttonElement = createTimestampButtonElement(handleClick); + try { + placeholderEl.replaceWith(buttonElement); + } catch (e) { + console.error("Error during replaceWith:", e); + } + }); + }); +} \ No newline at end of file diff --git a/src/webcomponents/TimestampUpdaterButton/timestampFileUpdater.ts b/src/webcomponents/TimestampUpdaterButton/timestampFileUpdater.ts new file mode 100644 index 0000000..a86f476 --- /dev/null +++ b/src/webcomponents/TimestampUpdaterButton/timestampFileUpdater.ts @@ -0,0 +1,88 @@ +import { App, Notice, TFile } from 'obsidian'; + +// Regex patterns are specific to this module's responsibility +const frontmatterRegex = /^---\s*([\s\S]*?)\s*---/; +const updatedAtRegexLine = /^(updatedAt:\s*)(.*)$/m; // Find existing line +const closingDashRegex = /^\s*---\s*$/; // To find the closing dashes + +interface UpdateResult { + success: boolean; + message: string; +} + +/** + * Reads the file, updates the 'updatedAt' timestamp in the frontmatter manually, + * and saves the file back. + * + * @param app Obsidian App instance + * @param file The TFile to modify + * @returns Promise resolving to an UpdateResult object + */ +export async function updateTimestampInFile(app: App, file: TFile): Promise { + try { + const content = await app.vault.read(file); + const match = content.match(frontmatterRegex); + + if (!match || !match[0]) { + return { success: false, message: 'No frontmatter found in this file.' }; + } + + const fullFrontmatterBlock = match[0]; + const frontmatterContent = match[1]; + const body = content.substring(fullFrontmatterBlock.length); + const newTimestamp = new Date().toISOString(); + + let existingTimestamp: string | null = null; + let updatedFrontmatterBlock: string; + + const updatedAtMatch = frontmatterContent.match(updatedAtRegexLine); + if (updatedAtMatch && updatedAtMatch[2]) { + existingTimestamp = updatedAtMatch[2].trim(); + } + + if (existingTimestamp === newTimestamp) { + return { success: true, message: `Timestamp already up-to-date (${newTimestamp.split('T')[1].substring(0, 8)}).` }; + } + + // Manual Update Logic + if (updatedAtMatch) { + // Found existing line: replace the value part + updatedFrontmatterBlock = fullFrontmatterBlock.replace( + updatedAtRegexLine, + `$1${newTimestamp}` + ); + } else { + // Didn't find the line: Append it before the closing '---' + const lines = fullFrontmatterBlock.trimEnd().split('\n'); + // Find the index of the closing '---' line + const closingDashIndex = lines.findIndex(line => closingDashRegex.test(line)); + + if (closingDashIndex > 0) { // Ensure closing dashes are found and not the first line + lines.splice(closingDashIndex, 0, `updatedAt: ${newTimestamp}`); // Insert before closing dashes + updatedFrontmatterBlock = lines.join('\n'); + // Add back potential trailing newline if original block had one + if (fullFrontmatterBlock.endsWith('\n') && !updatedFrontmatterBlock.endsWith('\n')) { + updatedFrontmatterBlock += '\n'; + } + } else { + console.warn(`FileUpdater: Could not find closing '---' to append timestamp in ${file.basename}`); + return { success: false, message: 'Could not reliably update frontmatter (structure error?).' }; + } + } + + // Reconstruct the full content + const newContent = updatedFrontmatterBlock + body; + + // Check if content actually changed before writing (prevents unnecessary file modifications) + if (newContent === content) { + return { success: true, message: `Timestamp already effectively up-to-date.` }; + } + + await app.vault.modify(file, newContent); + return { success: true, message: `Updated timestamp for ${file.basename}` }; + + } catch (error: any) { + console.error(`FileUpdater: Error processing file ${file.basename}`, error); + return { success: false, message: `Error updating timestamp: ${error.message}` }; + } +} \ No newline at end of file