From 0c59c1143e1823b0b537b79d37c25b12eb6622ed Mon Sep 17 00:00:00 2001 From: Guilherme Cattani Date: Mon, 24 Mar 2025 23:34:18 +0100 Subject: [PATCH 1/4] Improve settings examples, add specific namespace Add colorpicker to trail and bar color Add headings Fix error where bar type was set to 'forward' and that wasn't valid on first boot, making setting blank Fix examples using old custom format (parenthesis) Fix container namespaced class --- README.md | 2 +- main.ts | 90 ++++++++++++++++++++++++++++-------------------------- styles.css | 15 +++++---- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 5655534..259c7cd 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ endDate: 2025-04-11T14:00:00 type: Circle color: #ff5722 trailColor: #f5f5f5 -infoFormat: {percent}% complete - {remaining} until {end(LLL d, yyyy)} +infoFormat: {percent}% complete - {remaining} until {end:LLL d, yyyy} updateInRealTime: true updateInterval: 30 ``` diff --git a/main.ts b/main.ts index f5a55f2..7bf24f1 100644 --- a/main.ts +++ b/main.ts @@ -17,7 +17,7 @@ const DEFAULT_SETTINGS: ProgressBarSettings = { defaultBarColor: '#4CAF50', defaultTrailColor: '#e0e0e0', defaultBarType: 'Line', - defaultProgressType: 'forward', + defaultProgressType: 'Forward', defaultOnCompleteText: '{title} is done!', defaultInfoFormat: '{percent}% - {remaining} remaining', defaultUpdateInRealTime: false, @@ -62,14 +62,14 @@ class LuxonFormatHelpModal extends Modal { contentEl.createEl('a', { href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', text: 'Luxon Formatting Reference' }); contentEl.createEl('p', { text: 'For durations the only tokens that are supported are for days, hours, minutes and seconds:' }); - luxonList.createEl('li', { text: '{remaining(format)} - Format remaining time' }); - luxonList.createEl('li', { text: '{elapsed(format)} - Format elapsed time' }); - luxonList.createEl('li', { text: '{total(format)} - Format total duration' }); + luxonList.createEl('li', { text: '{remaining:format} - Format remaining time' }); + luxonList.createEl('li', { text: '{elapsed:format} - Format elapsed time' }); + luxonList.createEl('li', { text: '{total:format} - Format total duration' }); contentEl.createEl('h3', { text: 'Examples' }); const examplesList = contentEl.createEl('ul'); examplesList.createEl('li', { text: '{percent}% complete - {remaining} left' }); - examplesList.createEl('li', { text: 'Started on {start(LLL d)}, ends on {end(LLL d, yyyy)}' }); + examplesList.createEl('li', { text: 'Started on {start:LLL d}, ends on {end:LLL d, yyyy}' }); examplesList.createEl('li', { text: '{elapsed} elapsed out of {total} total' }); contentEl.createEl('h3', { text: 'Common Luxon Formats' }); @@ -109,6 +109,7 @@ class ProgressBarSettingTab extends PluginSettingTab { containerEl.empty(); containerEl.createEl('h2', { text: 'Countdown To Settings' }); + new Setting(containerEl).setName('Bar types').setHeading(); new Setting(containerEl) .setName('Default bar type') .setDesc('Default type of progress bar to display') @@ -135,28 +136,33 @@ class ProgressBarSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); - new Setting(containerEl) - .setName('Update in real-time') - .setDesc('Update progress bars according to the update interval') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.defaultUpdateInRealTime) - .onChange(async (value) => { - this.plugin.settings.defaultUpdateInRealTime = value; - await this.plugin.saveSettings(); - })); + new Setting(containerEl).setName('Real time update').setHeading(); + new Setting(containerEl) + .setName('Update in real-time') + .setDesc('Update progress bars according to the update interval') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.defaultUpdateInRealTime) + .onChange(async (value) => { + this.plugin.settings.defaultUpdateInRealTime = value; + await this.plugin.saveSettings(); + this.display(); + })); - new Setting(containerEl) - .setName('Update interval') - .setDesc('How often to update the progress bars (in seconds). This will affect performance.') - .addSlider(slider => slider - .setLimits(0.5, 20, 0.5) - .setValue(this.plugin.settings.defaultUpdateIntervalSeconds) - .setDynamicTooltip() - .onChange(async (value) => { - this.plugin.settings.defaultUpdateIntervalSeconds = value; - await this.plugin.saveSettings(); - })); + if (this.plugin.settings.defaultUpdateInRealTime) { + new Setting(containerEl) + .setName('Update interval') + .setDesc('How often to update the progress bars (in seconds). This will affect performance.') + .addSlider(slider => slider + .setLimits(0.5, 20, 0.5) + .setValue(this.plugin.settings.defaultUpdateIntervalSeconds) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.defaultUpdateIntervalSeconds = value; + await this.plugin.saveSettings(); + })); + } + new Setting(containerEl).setName('Text').setHeading(); new Setting(containerEl) .setName('Default info format') .setDesc('Default format for the info text. Uses Luxon formatting (See format help button for a quick reference).') @@ -174,18 +180,7 @@ class ProgressBarSettingTab extends PluginSettingTab { .onClick(() => { new LuxonFormatHelpModal(this.plugin.app).open(); }); - }); - - new Setting(containerEl) - .setName('Default bar color') - .setDesc('Default color for the progress bar') - .addText(text => text - .setPlaceholder('#4CAF50') - .setValue(this.plugin.settings.defaultBarColor) - .onChange(async (value) => { - this.plugin.settings.defaultBarColor = value; - await this.plugin.saveSettings(); - })); + }); new Setting(containerEl) .setName('Default on complete text') @@ -198,16 +193,27 @@ class ProgressBarSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + new Setting(containerEl).setName('Colors').setHeading(); + new Setting(containerEl) + .setName('Default bar color') + .setDesc('Default color for the progress bar') + .addColorPicker(color => color + .setValue(this.plugin.settings.defaultBarColor) + .onChange(async (value) => { + this.plugin.settings.defaultBarColor = value; + await this.plugin.saveSettings(); + })); + new Setting(containerEl) .setName('Default trail color') .setDesc('Default trail color for the progress bar (the incomplete part)') - .addText(text => text - .setPlaceholder('#e0e0e0') + .addColorPicker(color => color .setValue(this.plugin.settings.defaultTrailColor) .onChange(async (value) => { this.plugin.settings.defaultTrailColor = value; await this.plugin.saveSettings(); })); + containerEl.createEl('br'); containerEl.createEl('i', { text: 'All settings can be overridden in the markdown code block. If stuck please refer to the ' }); containerEl.createEl('a', { href: 'https://github.com/guicattani/countdown-to?tab=readme-ov-file#how-to-use', text: 'how to use guide' }); } @@ -232,10 +238,6 @@ export default class ProgressBarPlugin extends Plugin { }); } - onunload() { - this.cleanupAllProgressBars(); - } - async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } @@ -280,7 +282,7 @@ export default class ProgressBarPlugin extends Plugin { const params = this.parseProgressBarParams(source); el.empty(); - const containerEl = el.createDiv({ cls: 'countdown-to-container' }); + const containerEl = el.createDiv({ cls: ['countdown-to-plugin', 'countdown-to-container'] }); const startDate = DateTime.fromISO(params.startDate); const endDate = DateTime.fromISO(params.endDate); diff --git a/styles.css b/styles.css index 892e3e8..fc8f12e 100644 --- a/styles.css +++ b/styles.css @@ -1,18 +1,17 @@ -.countdown-to-container { - position: relative; +.countdown-to-plugin.countdown-to-container { margin: 20px 0; padding: 10px; border-radius: 5px; background-color: var(--background-secondary); } -.progress-bar-title { +.countdown-to-plugin .progress-bar-title { font-weight: bold; margin-bottom: 10px; text-align: center; } -.progress-bar-info { +.countdown-to-plugin .progress-bar-info { text-align: center; font-size: 0.9em; color: var(--text-muted); @@ -20,13 +19,13 @@ } /* Circle, SemiCircle and Square need more height */ -.progress-bar-circle, -.progress-bar-semicircle, -.progress-bar-square { +.countdown-to-plugin .progress-bar-circle, +.countdown-to-plugin .progress-bar-semicircle, +.countdown-to-plugin .progress-bar-square { height: 100px; } /* Line can stay with the default height */ -.progress-bar-line { +.countdown-to-plugin .progress-bar-line { height: 20px; } From 20d566aa54e193f3aa05a8788f49a0fc68e239c3 Mon Sep 17 00:00:00 2001 From: Guilherme Cattani Date: Mon, 24 Mar 2025 23:34:18 +0100 Subject: [PATCH 2/4] Change ProgressBar to CountdownTo for consistency Legacy from back when I didn't know what to call this --- README.md | 4 +- main.ts | 114 ++++++++++++++++++++++++++--------------------------- styles.css | 12 +++--- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 259c7cd..4f5251b 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ Track time until important deadlines, events, or milestones with visual progress ## How to Use -Create a countdown progress bar by adding a code block with the `progressbar` language identifier: +Create a countdown progress bar by adding a code block with the `countdown-to` language identifier: ````markdown -```progressbar +```countdown-to title: Project Deadline startDate: 2025-03-12T08:00:00 endDate: 2025-04-11T14:00:00 diff --git a/main.ts b/main.ts index 7bf24f1..c58b51b 100644 --- a/main.ts +++ b/main.ts @@ -2,7 +2,7 @@ import { App, Plugin, PluginSettingTab, Setting, Modal } from 'obsidian'; import { DateTime, Duration, Interval } from 'luxon'; import * as ProgressBar from 'progressbar.js'; -interface ProgressBarSettings { +interface CountdownToSettings { defaultBarColor: string; defaultTrailColor: string; defaultBarType: string; @@ -13,7 +13,7 @@ interface ProgressBarSettings { defaultUpdateIntervalSeconds: number; } -const DEFAULT_SETTINGS: ProgressBarSettings = { +const DEFAULT_SETTINGS: CountdownToSettings = { defaultBarColor: '#4CAF50', defaultTrailColor: '#e0e0e0', defaultBarType: 'Line', @@ -25,13 +25,13 @@ const DEFAULT_SETTINGS: ProgressBarSettings = { }; // Minimal interface for progressbar.js instances -interface ProgressBarJs { +interface CountdownToJs { set(progress: number): void; } -interface ProgressBarInstance { +interface CountdownToInstace { element: HTMLElement; - bar: ProgressBarJs; + bar: CountdownToJs; infoEl: HTMLElement; params: string; updateTimer: number | null; @@ -96,10 +96,10 @@ class LuxonFormatHelpModal extends Modal { } } -class ProgressBarSettingTab extends PluginSettingTab { - plugin: ProgressBarPlugin; +class CountdownToSettingTab extends PluginSettingTab { + plugin: CountdownToPlugin; - constructor(app: App, plugin: ProgressBarPlugin) { + constructor(app: App, plugin: CountdownToPlugin) { super(app, plugin); this.plugin = plugin; } @@ -219,22 +219,22 @@ class ProgressBarSettingTab extends PluginSettingTab { } } -export default class ProgressBarPlugin extends Plugin { - settings: ProgressBarSettings; +export default class CountdownToPlugin extends Plugin { + settings: CountdownToSettings; - progressBars = new Map(); + countdownTos = new Map(); async onload() { await this.loadSettings(); - this.registerMarkdownCodeBlockProcessor('progressbar', (source, el) => { + this.registerMarkdownCodeBlockProcessor('countdown-to', (source, el) => { const id = Math.random().toString(36).substring(2, 15); - this.renderProgressBar(source, el, id); + this.renderCountdownTo(source, el, id); }); - this.addSettingTab(new ProgressBarSettingTab(this.app, this)); + this.addSettingTab(new CountdownToSettingTab(this.app, this)); this.register(() => { - this.cleanupAllProgressBars(); + this.cleanupAllCountdownTos(); }); } @@ -244,42 +244,42 @@ export default class ProgressBarPlugin extends Plugin { async saveSettings() { await this.saveData(this.settings); - this.refreshAllProgressBars(); + this.refreshAllCountdownTos(); } - cleanupProgressBar(id: string) { - const progressBar = this.progressBars.get(id); - if (progressBar && progressBar.updateTimer) { - window.clearTimeout(progressBar.updateTimer); - this.progressBars.delete(id); + cleanupCountdownTo(id: string) { + const countdownTo = this.countdownTos.get(id); + if (countdownTo && countdownTo.updateTimer) { + window.clearTimeout(countdownTo.updateTimer); + this.countdownTos.delete(id); } } - cleanupAllProgressBars() { - this.progressBars.forEach((data) => { + cleanupAllCountdownTos() { + this.countdownTos.forEach((data) => { if (data.updateTimer) { window.clearTimeout(data.updateTimer); } }); - this.progressBars.clear(); + this.countdownTos.clear(); } - refreshAllProgressBars() { - this.progressBars.forEach((data, id) => { + refreshAllCountdownTos() { + this.countdownTos.forEach((data, id) => { if (data.updateTimer) { window.clearTimeout(data.updateTimer); data.updateTimer = null; } - this.renderProgressBar(data.params, data.element, id); + this.renderCountdownTo(data.params, data.element, id); }); } - renderProgressBar(source: string, el: HTMLElement, id: string) { + renderCountdownTo(source: string, el: HTMLElement, id: string) { try { - this.cleanupProgressBar(id); + this.cleanupCountdownTo(id); - const params = this.parseProgressBarParams(source); + const params = this.parseCountdownToParams(source); el.empty(); const containerEl = el.createDiv({ cls: ['countdown-to-plugin', 'countdown-to-container'] }); @@ -304,11 +304,11 @@ export default class ProgressBarPlugin extends Plugin { return; } - const progressBarEl = containerEl.createDiv({ cls: 'progress-bar-element' }); + const countdownToEl = containerEl.createDiv({ cls: 'countdown-to-element' }); const barType = params.type || this.settings.defaultBarType; - progressBarEl.addClass(`progress-bar-${barType.toLowerCase()}`); + countdownToEl.addClass(`countdown-to-${barType.toLowerCase()}`); - const infoEl = containerEl.createDiv({ cls: 'progress-bar-info' }); + const infoEl = containerEl.createDiv({ cls: 'countdown-to-info' }); let bar; const barColor = params.color || this.settings.defaultBarColor; @@ -322,26 +322,26 @@ export default class ProgressBarPlugin extends Plugin { switch (barType.toLowerCase()) { case 'circle': - bar = new ProgressBar.Circle(progressBarEl, { + bar = new ProgressBar.Circle(countdownToEl, { ...commonOptions, svgStyle: { width: '100%', height: '100%' }, }); break; case 'semicircle': - bar = new ProgressBar.SemiCircle(progressBarEl, { + bar = new ProgressBar.SemiCircle(countdownToEl, { ...commonOptions, svgStyle: { width: '100%', height: '100%' }, }); break; case 'square': - bar = new ProgressBar.Square(progressBarEl, { + bar = new ProgressBar.Square(countdownToEl, { ...commonOptions, svgStyle: { width: '100%', height: '100%' }, }); break; case 'line': default: - bar = new ProgressBar.Line(progressBarEl, { + bar = new ProgressBar.Line(countdownToEl, { ...commonOptions, svgStyle: { width: '100%', height: '100%' }, }); @@ -349,12 +349,12 @@ export default class ProgressBarPlugin extends Plugin { } if (params.title) { - const titleEl = containerEl.createDiv({ cls: 'progress-bar-title' }); + const titleEl = containerEl.createDiv({ cls: 'countdown-to-title' }); titleEl.setText(params.title); containerEl.prepend(titleEl); } - this.progressBars.set(id, { + this.countdownTos.set(id, { element: el, bar: bar, infoEl: infoEl, @@ -362,7 +362,7 @@ export default class ProgressBarPlugin extends Plugin { updateTimer: null, }); - this.updateProgressBar(id, startDate, endDate); + this.updateCountdownTo(id, startDate, endDate); const updateInRealTime = params.updateInRealTime !== undefined ? params.updateInRealTime === 'true' : @@ -377,35 +377,35 @@ export default class ProgressBarPlugin extends Plugin { this.scheduleUpdate(id, startDate, endDate, updateInterval); }, updateInterval * 1000); - const progressBarInstance = this.progressBars.get(id); - if (progressBarInstance) { - progressBarInstance.updateTimer = timer; + const CountdownToInstace = this.countdownTos.get(id); + if (CountdownToInstace) { + CountdownToInstace.updateTimer = timer; } } } catch (error) { - el.setText('Error rendering progress bar: ' + error.message); + el.setText('Error rendering countdown to: ' + error.message); } } scheduleUpdate(id: string, startDate: DateTime, endDate: DateTime, defaultUpdateIntervalSeconds: number) { - const progressBar = this.progressBars.get(id); - if (!progressBar) return; + const countdownTo = this.countdownTos.get(id); + if (!countdownTo) return; - this.updateProgressBar(id, startDate, endDate); + this.updateCountdownTo(id, startDate, endDate); const timer = window.setTimeout(() => { this.scheduleUpdate(id, startDate, endDate, defaultUpdateIntervalSeconds); }, defaultUpdateIntervalSeconds * 1000); - progressBar.updateTimer = timer; + countdownTo.updateTimer = timer; } - updateProgressBar(id: string, startDate: DateTime, endDate: DateTime) { - const progressBar = this.progressBars.get(id); - if (!progressBar) return; + updateCountdownTo(id: string, startDate: DateTime, endDate: DateTime) { + const countdownTo = this.countdownTos.get(id); + if (!countdownTo) return; - const params = this.parseProgressBarParams(progressBar.params); + const params = this.parseCountdownToParams(countdownTo.params); const currentDate = DateTime.now(); const totalInterval = Interval.fromDateTimes(startDate, endDate); @@ -420,13 +420,13 @@ export default class ProgressBarPlugin extends Plugin { const infoFormat = params.infoFormat || this.settings.defaultInfoFormat; if (progressType.toLowerCase() === 'countdown') { - progressBar.bar.set(1.0 - progress); + countdownTo.bar.set(1.0 - progress); } else { - progressBar.bar.set(Math.floor(progress * 100) / 100); + countdownTo.bar.set(Math.floor(progress * 100) / 100); } if (progress >= 1) { - progressBar.infoEl.setText( + countdownTo.infoEl.setText( onCompleteText.replace(/{title}/g, params.title || ''), ); } else { @@ -455,11 +455,11 @@ export default class ProgressBarPlugin extends Plugin { .replace(/{elapsed}/g, this.formatDuration(elapsedDuration)) .replace(/{total}/g, this.formatDuration(totalDuration)); - progressBar.infoEl.setText(infoText); + countdownTo.infoEl.setText(infoText); } } - parseProgressBarParams(source: string): Record { + parseCountdownToParams(source: string): Record { const params: Record = {}; const lines = source.trim().split('\n'); diff --git a/styles.css b/styles.css index fc8f12e..d5bf385 100644 --- a/styles.css +++ b/styles.css @@ -5,13 +5,13 @@ background-color: var(--background-secondary); } -.countdown-to-plugin .progress-bar-title { +.countdown-to-plugin .countdown-to-title { font-weight: bold; margin-bottom: 10px; text-align: center; } -.countdown-to-plugin .progress-bar-info { +.countdown-to-plugin .countdown-to-info { text-align: center; font-size: 0.9em; color: var(--text-muted); @@ -19,13 +19,13 @@ } /* Circle, SemiCircle and Square need more height */ -.countdown-to-plugin .progress-bar-circle, -.countdown-to-plugin .progress-bar-semicircle, -.countdown-to-plugin .progress-bar-square { +.countdown-to-plugin .countdown-to-circle, +.countdown-to-plugin .countdown-to-semicircle, +.countdown-to-plugin .countdown-to-square { height: 100px; } /* Line can stay with the default height */ -.countdown-to-plugin .progress-bar-line { +.countdown-to-plugin .countdown-to-line { height: 20px; } From d0fc498160178c2df5db8134372b1c47432a8a20 Mon Sep 17 00:00:00 2001 From: Guilherme Cattani Date: Wed, 26 Mar 2025 20:54:30 +0100 Subject: [PATCH 3/4] Add markdownrenderchild New folder structure based on https://github.com/Zachatoo/obsidian-rerender-markdown-codeblock-processor-example Improve modal Fix tabs indentation Fix tabs identation --- esbuild.config.mjs | 2 +- main.ts | 508 -------------------------- src/countdownToMarkdownRenderChild.ts | 262 +++++++++++++ src/main.ts | 61 ++++ src/modal.ts | 70 ++++ src/obsidian.d.ts | 10 + src/settings.ts | 174 +++++++++ 7 files changed, 578 insertions(+), 509 deletions(-) delete mode 100644 main.ts create mode 100644 src/countdownToMarkdownRenderChild.ts create mode 100644 src/main.ts create mode 100644 src/modal.ts create mode 100644 src/obsidian.d.ts create mode 100644 src/settings.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a5de8b8..309f17b 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -15,7 +15,7 @@ const context = await esbuild.context({ banner: { js: banner, }, - entryPoints: ["main.ts"], + entryPoints: ["src/main.ts"], bundle: true, external: [ "obsidian", diff --git a/main.ts b/main.ts deleted file mode 100644 index c58b51b..0000000 --- a/main.ts +++ /dev/null @@ -1,508 +0,0 @@ -import { App, Plugin, PluginSettingTab, Setting, Modal } from 'obsidian'; -import { DateTime, Duration, Interval } from 'luxon'; -import * as ProgressBar from 'progressbar.js'; - -interface CountdownToSettings { - defaultBarColor: string; - defaultTrailColor: string; - defaultBarType: string; - defaultProgressType: string; - defaultOnCompleteText: string; - defaultInfoFormat: string; - defaultUpdateInRealTime: boolean; - defaultUpdateIntervalSeconds: number; -} - -const DEFAULT_SETTINGS: CountdownToSettings = { - defaultBarColor: '#4CAF50', - defaultTrailColor: '#e0e0e0', - defaultBarType: 'Line', - defaultProgressType: 'Forward', - defaultOnCompleteText: '{title} is done!', - defaultInfoFormat: '{percent}% - {remaining} remaining', - defaultUpdateInRealTime: false, - defaultUpdateIntervalSeconds: 1, -}; - -// Minimal interface for progressbar.js instances -interface CountdownToJs { - set(progress: number): void; -} - -interface CountdownToInstace { - element: HTMLElement; - bar: CountdownToJs; - infoEl: HTMLElement; - params: string; - updateTimer: number | null; -} - -class LuxonFormatHelpModal extends Modal { - onOpen() { - const { contentEl } = this; - - contentEl.createEl('h2', { text: 'Info Format Help' }); - - contentEl.createEl('h3', { text: 'Placeholders' }); - const placeholdersList = contentEl.createEl('ul'); - placeholdersList.createEl('li', { text: '{percent} - Percentage of completion' }); - placeholdersList.createEl('li', { text: '{start} - Start date (ISO format)' }); - placeholdersList.createEl('li', { text: '{end} - End date (ISO format)' }); - placeholdersList.createEl('li', { text: '{current} - Current date (ISO format)' }); - placeholdersList.createEl('li', { text: '{remaining} - Remaining time' }); - placeholdersList.createEl('li', { text: '{elapsed} - Elapsed time' }); - placeholdersList.createEl('li', { text: '{total} - Total duration' }); - - contentEl.createEl('h3', { text: 'Formatting' }); - contentEl.createEl('p', { text: 'You can use Luxon formatting for dates:' }); - const luxonList = contentEl.createEl('ul'); - luxonList.createEl('li', { text: '{start(format)} - Format start date' }); - luxonList.createEl('li', { text: '{end(format)} - Format end date' }); - luxonList.createEl('li', { text: '{current(format)} - Format current date' }); - contentEl.createEl('a', { href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', text: 'Luxon Formatting Reference' }); - - contentEl.createEl('p', { text: 'For durations the only tokens that are supported are for days, hours, minutes and seconds:' }); - luxonList.createEl('li', { text: '{remaining:format} - Format remaining time' }); - luxonList.createEl('li', { text: '{elapsed:format} - Format elapsed time' }); - luxonList.createEl('li', { text: '{total:format} - Format total duration' }); - - contentEl.createEl('h3', { text: 'Examples' }); - const examplesList = contentEl.createEl('ul'); - examplesList.createEl('li', { text: '{percent}% complete - {remaining} left' }); - examplesList.createEl('li', { text: 'Started on {start:LLL d}, ends on {end:LLL d, yyyy}' }); - examplesList.createEl('li', { text: '{elapsed} elapsed out of {total} total' }); - - contentEl.createEl('h3', { text: 'Common Luxon Formats' }); - const formatsTable = contentEl.createEl('table'); - const headerRow = formatsTable.createEl('tr'); - headerRow.createEl('th', { text: 'Format' }); - headerRow.createEl('th', { text: 'Output' }); - - const addFormatRow = (format: string, output: string) => { - const row = formatsTable.createEl('tr'); - row.createEl('td', { text: format }); - row.createEl('td', { text: output }); - }; - - addFormatRow('yyyy-MM-dd', '2025-04-11'); - addFormatRow('LLL d, yyyy', 'Apr 11, 2025'); - addFormatRow('EEEE, MMMM d, yyyy', 'Thursday, April 11, 2025'); - addFormatRow('d MMMM yyyy', '11 April 2025'); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} - -class CountdownToSettingTab extends PluginSettingTab { - plugin: CountdownToPlugin; - - constructor(app: App, plugin: CountdownToPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - containerEl.empty(); - containerEl.createEl('h2', { text: 'Countdown To Settings' }); - - new Setting(containerEl).setName('Bar types').setHeading(); - new Setting(containerEl) - .setName('Default bar type') - .setDesc('Default type of progress bar to display') - .addDropdown(dropdown => dropdown - .addOption('Line', 'Line') - .addOption('Circle', 'Circle') - .addOption('SemiCircle', 'Semi Circle') - .addOption('Square', 'Square') - .setValue(this.plugin.settings.defaultBarType) - .onChange(async (value) => { - this.plugin.settings.defaultBarType = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Default progress type') - .setDesc('Count as progress or as a countdown') - .addDropdown(dropdown => dropdown - .addOption('Progress', 'Progress') - .addOption('Countdown', 'Countdown') - .setValue(this.plugin.settings.defaultProgressType) - .onChange(async (value) => { - this.plugin.settings.defaultProgressType = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl).setName('Real time update').setHeading(); - new Setting(containerEl) - .setName('Update in real-time') - .setDesc('Update progress bars according to the update interval') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.defaultUpdateInRealTime) - .onChange(async (value) => { - this.plugin.settings.defaultUpdateInRealTime = value; - await this.plugin.saveSettings(); - this.display(); - })); - - if (this.plugin.settings.defaultUpdateInRealTime) { - new Setting(containerEl) - .setName('Update interval') - .setDesc('How often to update the progress bars (in seconds). This will affect performance.') - .addSlider(slider => slider - .setLimits(0.5, 20, 0.5) - .setValue(this.plugin.settings.defaultUpdateIntervalSeconds) - .setDynamicTooltip() - .onChange(async (value) => { - this.plugin.settings.defaultUpdateIntervalSeconds = value; - await this.plugin.saveSettings(); - })); - } - - new Setting(containerEl).setName('Text').setHeading(); - new Setting(containerEl) - .setName('Default info format') - .setDesc('Default format for the info text. Uses Luxon formatting (See format help button for a quick reference).') - .addTextArea(text => text - .setPlaceholder('{percent}% - {remaining} remaining') - .setValue(this.plugin.settings.defaultInfoFormat) - .onChange(async (value) => { - this.plugin.settings.defaultInfoFormat = value; - await this.plugin.saveSettings(); - })) - .addExtraButton(button => { - button - .setIcon('help') - .setTooltip('Show format help') - .onClick(() => { - new LuxonFormatHelpModal(this.plugin.app).open(); - }); - }); - - new Setting(containerEl) - .setName('Default on complete text') - .setDesc('Default text to display when the progress is complete. Use {title} to display the title of the progress bar.') - .addText(text => text - .setPlaceholder('{title} is done!') - .setValue(this.plugin.settings.defaultOnCompleteText) - .onChange(async (value) => { - this.plugin.settings.defaultOnCompleteText = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl).setName('Colors').setHeading(); - new Setting(containerEl) - .setName('Default bar color') - .setDesc('Default color for the progress bar') - .addColorPicker(color => color - .setValue(this.plugin.settings.defaultBarColor) - .onChange(async (value) => { - this.plugin.settings.defaultBarColor = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Default trail color') - .setDesc('Default trail color for the progress bar (the incomplete part)') - .addColorPicker(color => color - .setValue(this.plugin.settings.defaultTrailColor) - .onChange(async (value) => { - this.plugin.settings.defaultTrailColor = value; - await this.plugin.saveSettings(); - })); - containerEl.createEl('br'); - containerEl.createEl('i', { text: 'All settings can be overridden in the markdown code block. If stuck please refer to the ' }); - containerEl.createEl('a', { href: 'https://github.com/guicattani/countdown-to?tab=readme-ov-file#how-to-use', text: 'how to use guide' }); - } -} - -export default class CountdownToPlugin extends Plugin { - settings: CountdownToSettings; - - countdownTos = new Map(); - - async onload() { - await this.loadSettings(); - - this.registerMarkdownCodeBlockProcessor('countdown-to', (source, el) => { - const id = Math.random().toString(36).substring(2, 15); - this.renderCountdownTo(source, el, id); - }); - - this.addSettingTab(new CountdownToSettingTab(this.app, this)); - this.register(() => { - this.cleanupAllCountdownTos(); - }); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - this.refreshAllCountdownTos(); - } - - cleanupCountdownTo(id: string) { - const countdownTo = this.countdownTos.get(id); - if (countdownTo && countdownTo.updateTimer) { - window.clearTimeout(countdownTo.updateTimer); - this.countdownTos.delete(id); - } - } - - cleanupAllCountdownTos() { - this.countdownTos.forEach((data) => { - if (data.updateTimer) { - window.clearTimeout(data.updateTimer); - } - }); - this.countdownTos.clear(); - } - - refreshAllCountdownTos() { - this.countdownTos.forEach((data, id) => { - if (data.updateTimer) { - window.clearTimeout(data.updateTimer); - data.updateTimer = null; - } - - this.renderCountdownTo(data.params, data.element, id); - }); - } - - renderCountdownTo(source: string, el: HTMLElement, id: string) { - try { - this.cleanupCountdownTo(id); - - const params = this.parseCountdownToParams(source); - - el.empty(); - const containerEl = el.createDiv({ cls: ['countdown-to-plugin', 'countdown-to-container'] }); - - const startDate = DateTime.fromISO(params.startDate); - const endDate = DateTime.fromISO(params.endDate); - - if (!startDate.isValid) { - containerEl.setText('Invalid start date format. ' + - 'Please use ISO + time format (YYYY-MM-DDTHH:MM:SS).'); - return; - } - - if (!endDate.isValid) { - containerEl.setText('Invalid end date format. ' + - 'Please use ISO + time format (YYYY-MM-DDTHH:MM:SS).'); - return; - } - - if (endDate < startDate) { - containerEl.setText('End date must be after start date.'); - return; - } - - const countdownToEl = containerEl.createDiv({ cls: 'countdown-to-element' }); - const barType = params.type || this.settings.defaultBarType; - countdownToEl.addClass(`countdown-to-${barType.toLowerCase()}`); - - const infoEl = containerEl.createDiv({ cls: 'countdown-to-info' }); - - let bar; - const barColor = params.color || this.settings.defaultBarColor; - const trailColor = params.trailColor || this.settings.defaultTrailColor; - const commonOptions = { - strokeWidth: 4, - color: barColor, - trailColor: trailColor, - trailWidth: 1, - }; - - switch (barType.toLowerCase()) { - case 'circle': - bar = new ProgressBar.Circle(countdownToEl, { - ...commonOptions, - svgStyle: { width: '100%', height: '100%' }, - }); - break; - case 'semicircle': - bar = new ProgressBar.SemiCircle(countdownToEl, { - ...commonOptions, - svgStyle: { width: '100%', height: '100%' }, - }); - break; - case 'square': - bar = new ProgressBar.Square(countdownToEl, { - ...commonOptions, - svgStyle: { width: '100%', height: '100%' }, - }); - break; - case 'line': - default: - bar = new ProgressBar.Line(countdownToEl, { - ...commonOptions, - svgStyle: { width: '100%', height: '100%' }, - }); - break; - } - - if (params.title) { - const titleEl = containerEl.createDiv({ cls: 'countdown-to-title' }); - titleEl.setText(params.title); - containerEl.prepend(titleEl); - } - - this.countdownTos.set(id, { - element: el, - bar: bar, - infoEl: infoEl, - params: source, - updateTimer: null, - }); - - this.updateCountdownTo(id, startDate, endDate); - - const updateInRealTime = params.updateInRealTime !== undefined ? - params.updateInRealTime === 'true' : - this.settings.defaultUpdateInRealTime; - - if (updateInRealTime) { - const updateInterval = params.updateInterval ? - parseInt(params.updateInterval, 10) : - this.settings.defaultUpdateIntervalSeconds; - - const timer = window.setTimeout(() => { - this.scheduleUpdate(id, startDate, endDate, updateInterval); - }, updateInterval * 1000); - - const CountdownToInstace = this.countdownTos.get(id); - if (CountdownToInstace) { - CountdownToInstace.updateTimer = timer; - } - } - - } catch (error) { - el.setText('Error rendering countdown to: ' + error.message); - } - } - - scheduleUpdate(id: string, startDate: DateTime, endDate: DateTime, defaultUpdateIntervalSeconds: number) { - const countdownTo = this.countdownTos.get(id); - if (!countdownTo) return; - - this.updateCountdownTo(id, startDate, endDate); - - const timer = window.setTimeout(() => { - this.scheduleUpdate(id, startDate, endDate, defaultUpdateIntervalSeconds); - }, defaultUpdateIntervalSeconds * 1000); - - countdownTo.updateTimer = timer; - } - - updateCountdownTo(id: string, startDate: DateTime, endDate: DateTime) { - const countdownTo = this.countdownTos.get(id); - if (!countdownTo) return; - - const params = this.parseCountdownToParams(countdownTo.params); - const currentDate = DateTime.now(); - - const totalInterval = Interval.fromDateTimes(startDate, endDate); - const elapsedInterval = Interval.fromDateTimes(startDate, currentDate); - - const totalMillis = totalInterval.length(); - const elapsedMillis = Math.min(elapsedInterval.length(), totalMillis); - - const progress = Math.min(Math.max(elapsedMillis / totalMillis, 0), 1); - const progressType = params.progressType || this.settings.defaultProgressType; - const onCompleteText = params.onCompleteText || this.settings.defaultOnCompleteText; - const infoFormat = params.infoFormat || this.settings.defaultInfoFormat; - - if (progressType.toLowerCase() === 'countdown') { - countdownTo.bar.set(1.0 - progress); - } else { - countdownTo.bar.set(Math.floor(progress * 100) / 100); - } - - if (progress >= 1) { - countdownTo.infoEl.setText( - onCompleteText.replace(/{title}/g, params.title || ''), - ); - } else { - let infoText = infoFormat; - - const remainingInterval = Interval.fromDateTimes(currentDate, endDate); - const remainingDuration = remainingInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); - - const elapsedDuration = elapsedInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); - - const totalDuration = totalInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); - infoText = infoText - .replace(/{start:(.*?)}/g, (_match: string, format: string) => startDate.toFormat(format)) - .replace(/{end:(.*?)}/g, (_match: string, format: string) => endDate.toFormat(format)) - .replace(/{current:(.*?)}/g, (_match: string, format: string) => currentDate.toFormat(format)) - .replace(/{remaining:(.*?)}/g, (_match: string, format: string) => remainingDuration.toFormat(format)) - .replace(/{elapsed:(.*?)}/g, (_match: string, format: string) => elapsedDuration.toFormat(format)) - .replace(/{total:(.*?)}/g, (_match: string, format: string) => totalDuration.toFormat(format)); - - infoText = infoText - .replace(/{percent}/g, Math.floor(progress * 100).toString()) - .replace(/{start}/g, startDate.toISODate() || '') - .replace(/{end}/g, endDate.toISODate() || '') - .replace(/{current}/g, currentDate.toISODate() || '') - .replace(/{remaining}/g, this.formatDuration(remainingDuration)) - .replace(/{elapsed}/g, this.formatDuration(elapsedDuration)) - .replace(/{total}/g, this.formatDuration(totalDuration)); - - countdownTo.infoEl.setText(infoText); - } - } - - parseCountdownToParams(source: string): Record { - const params: Record = {}; - const lines = source.trim().split('\n'); - - lines.forEach(line => { - const colonIndex = line.indexOf(':'); - if (colonIndex !== -1) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - if (key && value) { - params[key] = value; - } - } - }); - - if (!params.startDate) { - throw new Error('Start date is required'); - } - - if (DateTime.fromISO(params.startDate) > DateTime.now()) { - throw new Error('Start date must be now or in the past'); - } - - if (!params.endDate) { - throw new Error('End date is required'); - } - - return params; - } - - formatDuration(duration: Duration): string { - const days = Math.ceil(duration.as('days')); - const hours = Math.ceil(duration.as('hours') % 24); - const minutes = Math.ceil(duration.as('minutes') % 60); - const seconds = Math.ceil(duration.as('seconds') % 60); - - if (days > 0) { - return `${days} day${days > 1 ? 's' : ''}`; - } else if (hours > 0) { - return `${hours} hour${hours > 1 ? 's' : ''}`; - } else if (minutes > 0) { - return `${minutes} minute${minutes > 1 ? 's' : ''}`; - } else { - return `${seconds} second${seconds > 1 ? 's' : ''}`; - } - } -} diff --git a/src/countdownToMarkdownRenderChild.ts b/src/countdownToMarkdownRenderChild.ts new file mode 100644 index 0000000..f1e1cc2 --- /dev/null +++ b/src/countdownToMarkdownRenderChild.ts @@ -0,0 +1,262 @@ +import { MarkdownRenderChild } from "obsidian"; +import CountdownToPlugin from "./main"; + +import { DateTime, Duration, Interval } from 'luxon'; +import * as ProgressBar from 'progressbar.js'; + +export class CountdownToMarkdownRenderChild extends MarkdownRenderChild { + plugin: CountdownToPlugin; + source: string; + id: string; + constructor( + plugin: CountdownToPlugin, + source: string, + containerEl: HTMLElement, + id: string + ) { + super(containerEl); + this.plugin = plugin; + this.id = id; + this.source = source; + this.display(); + } + + onload() { + this.registerEvent( + this.plugin.app.workspace.on( + "countdown-to:rerender", + this.display.bind(this) + ) + ); + } + + display() { + try { + const params = this.parseCountdownToParams(this.source); + + this.containerEl.empty(); + const containerEl = this.containerEl.createDiv({ cls: ['countdown-to-plugin', 'countdown-to-container'] }); + + const startDate = DateTime.fromISO(params.startDate); + const endDate = DateTime.fromISO(params.endDate); + + if (!startDate.isValid) { + containerEl.setText('Invalid start date format. ' + + 'Please use ISO + time format (YYYY-MM-DDTHH:MM:SS).'); + return; + } + + if (!endDate.isValid) { + containerEl.setText('Invalid end date format. ' + + 'Please use ISO + time format (YYYY-MM-DDTHH:MM:SS).'); + return; + } + + if (endDate < startDate) { + containerEl.setText('End date must be after start date.'); + return; + } + + const countdownToEl = containerEl.createDiv({ cls: 'countdown-to-element' }); + const barType = params.type || this.plugin.settings.defaultBarType; + countdownToEl.addClass(`countdown-to-${barType.toLowerCase()}`); + + const infoEl = containerEl.createDiv({ cls: 'countdown-to-info' }); + + let bar; + const barColor = params.color || this.plugin.settings.defaultBarColor; + const trailColor = params.trailColor || this.plugin.settings.defaultTrailColor; + const commonOptions = { + strokeWidth: 4, + color: barColor, + trailColor: trailColor, + trailWidth: 1, + }; + + switch (barType.toLowerCase()) { + case 'circle': + bar = new ProgressBar.Circle(countdownToEl, { + ...commonOptions, + svgStyle: { width: '100%', height: '100%' }, + }); + break; + case 'semicircle': + bar = new ProgressBar.SemiCircle(countdownToEl, { + ...commonOptions, + svgStyle: { width: '100%', height: '100%' }, + }); + break; + case 'square': + bar = new ProgressBar.Square(countdownToEl, { + ...commonOptions, + svgStyle: { width: '100%', height: '100%' }, + }); + break; + case 'line': + default: + bar = new ProgressBar.Line(countdownToEl, { + ...commonOptions, + svgStyle: { width: '100%', height: '100%' }, + }); + break; + } + + if (params.title) { + const titleEl = containerEl.createDiv({ cls: 'countdown-to-title' }); + titleEl.setText(params.title); + containerEl.prepend(titleEl); + } + + this.plugin.countdownTos.set(this.id, { + element: this.containerEl, + bar: bar, + infoEl: infoEl, + params: this.source, + updateTimer: null, + }); + + this.updateCountdownTo(this.id, startDate, endDate); + + const updateInRealTime = params.updateInRealTime !== undefined ? + params.updateInRealTime === 'true' : + this.plugin.settings.defaultUpdateInRealTime; + + if (updateInRealTime) { + const updateInterval = params.updateInterval ? + parseInt(params.updateInterval, 10) : + this.plugin.settings.defaultUpdateIntervalSeconds; + + const timer = window.setTimeout(() => { + this.scheduleUpdate(this.id, startDate, endDate, updateInterval); + }, updateInterval * 1000); + + const CountdownToInstace = this.plugin.countdownTos.get(this.id); + if (CountdownToInstace) { + CountdownToInstace.updateTimer = timer; + } + } + + } catch (error) { + this.containerEl.setText('Error rendering countdown to: ' + error.message); + } + } + + scheduleUpdate(id: string, startDate: DateTime, endDate: DateTime, defaultUpdateIntervalSeconds: number) { + const countdownTo = this.plugin.countdownTos.get(id); + if (!countdownTo) return; + + this.updateCountdownTo(id, startDate, endDate); + + const timer = window.setTimeout(() => { + this.scheduleUpdate(id, startDate, endDate, defaultUpdateIntervalSeconds); + }, defaultUpdateIntervalSeconds * 1000); + + countdownTo.updateTimer = timer; + } + + updateCountdownTo(id: string, startDate: DateTime, endDate: DateTime) { + const countdownTo = this.plugin.countdownTos.get(id); + if (!countdownTo) return; + + const params = this.parseCountdownToParams(countdownTo.params); + const currentDate = DateTime.now(); + + const totalInterval = Interval.fromDateTimes(startDate, endDate); + const elapsedInterval = Interval.fromDateTimes(startDate, currentDate); + + const totalMillis = totalInterval.length(); + const elapsedMillis = Math.min(elapsedInterval.length(), totalMillis); + + const progress = Math.min(Math.max(elapsedMillis / totalMillis, 0), 1); + const progressType = params.progressType || this.plugin.settings.defaultProgressType; + const onCompleteText = params.onCompleteText || this.plugin.settings.defaultOnCompleteText; + const infoFormat = params.infoFormat || this.plugin.settings.defaultInfoFormat; + + if (progressType.toLowerCase() === 'countdown') { + countdownTo.bar.set(1.0 - progress); + } else { + countdownTo.bar.set(Math.floor(progress * 100) / 100); + } + + if (progress >= 1) { + countdownTo.infoEl.setText( + onCompleteText.replace(/{title}/g, params.title || ''), + ); + } else { + let infoText = infoFormat; + + const remainingInterval = Interval.fromDateTimes(currentDate, endDate); + const remainingDuration = remainingInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); + + const elapsedDuration = elapsedInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); + + const totalDuration = totalInterval.toDuration(['days', 'hours', 'minutes', 'seconds']); + infoText = infoText + .replace(/{start:(.*?)}/g, (_match: string, format: string) => startDate.toFormat(format)) + .replace(/{end:(.*?)}/g, (_match: string, format: string) => endDate.toFormat(format)) + .replace(/{current:(.*?)}/g, (_match: string, format: string) => currentDate.toFormat(format)) + .replace(/{remaining:(.*?)}/g, (_match: string, format: string) => remainingDuration.toFormat(format)) + .replace(/{elapsed:(.*?)}/g, (_match: string, format: string) => elapsedDuration.toFormat(format)) + .replace(/{total:(.*?)}/g, (_match: string, format: string) => totalDuration.toFormat(format)); + + infoText = infoText + .replace(/{percent}/g, Math.floor(progress * 100).toString()) + .replace(/{start}/g, startDate.toISODate() || '') + .replace(/{end}/g, endDate.toISODate() || '') + .replace(/{current}/g, currentDate.toISODate() || '') + .replace(/{remaining}/g, this.formatDuration(remainingDuration)) + .replace(/{elapsed}/g, this.formatDuration(elapsedDuration)) + .replace(/{total}/g, this.formatDuration(totalDuration)); + + countdownTo.infoEl.setText(infoText); + } + } + + parseCountdownToParams(source: string): Record { + const params: Record = {}; + const lines = source.trim().split('\n'); + + lines.forEach(line => { + const colonIndex = line.indexOf(':'); + if (colonIndex !== -1) { + const key = line.substring(0, colonIndex).trim(); + const value = line.substring(colonIndex + 1).trim(); + if (key && value) { + params[key] = value; + } + } + }); + + if (!params.startDate) { + throw new Error('Start date is required'); + } + + if (DateTime.fromISO(params.startDate) > DateTime.now()) { + throw new Error('Start date must be now or in the past'); + } + + if (!params.endDate) { + throw new Error('End date is required'); + } + + return params; + } + + formatDuration(duration: Duration): string { + const days = Math.ceil(duration.as('days')); + const hours = Math.ceil(duration.as('hours') % 24); + const minutes = Math.ceil(duration.as('minutes') % 60); + const seconds = Math.ceil(duration.as('seconds') % 60); + + if (days > 0) { + return `${days} day${days > 1 ? 's' : ''}`; + } else if (hours > 0) { + return `${hours} hour${hours > 1 ? 's' : ''}`; + } else if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? 's' : ''}`; + } else { + return `${seconds} second${seconds > 1 ? 's' : ''}`; + } + } + +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..8a8d827 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,61 @@ +import { Plugin } from 'obsidian'; +import { CountdownToSettings, CountdownToSettingTab, DEFAULT_SETTINGS } from './settings'; +import { CountdownToMarkdownRenderChild } from './countdownToMarkdownRenderChild'; + +// Minimal interface for progressbar.js instances +interface CountdownToJs { + set(progress: number): void; +} + +interface CountdownToInstace { + element: HTMLElement; + bar: CountdownToJs; + infoEl: HTMLElement; + params: string; + updateTimer: number | null; +} + +export default class CountdownToPlugin extends Plugin { + settings: CountdownToSettings; + countdownTos = new Map(); + + async onload() { + await this.loadSettings(); + + this.registerMarkdownCodeBlockProcessor('countdown-to', (source, el, ctx) => { + const id = Math.random().toString(36).substring(2, 15); + + ctx.addChild(new CountdownToMarkdownRenderChild(this, source, el, id)); + }); + + this.addSettingTab(new CountdownToSettingTab(this.app, this)); + this.register(() => { + this.cleanupAllCountdownTos(); + }); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } + + cleanupCountdownTo(id: string) { + const countdownTo = this.countdownTos.get(id); + if (countdownTo && countdownTo.updateTimer) { + window.clearTimeout(countdownTo.updateTimer); + this.countdownTos.delete(id); + } + } + + cleanupAllCountdownTos() { + this.countdownTos.forEach((data) => { + if (data.updateTimer) { + window.clearTimeout(data.updateTimer); + } + }); + this.countdownTos.clear(); + } +} diff --git a/src/modal.ts b/src/modal.ts new file mode 100644 index 0000000..9107374 --- /dev/null +++ b/src/modal.ts @@ -0,0 +1,70 @@ +import { Modal } from 'obsidian'; + +export class LuxonFormatHelpModal extends Modal { + onOpen() { + const { contentEl } = this; + + contentEl.createEl('h2', { text: 'Info Format Help' }); + + contentEl.createEl('h3', { text: 'Placeholders' }); + const placeholdersList = contentEl.createEl('ul'); + placeholdersList.createEl('li', { text: '{percent} - Percentage of completion' }); + placeholdersList.createEl('li', { text: '{start} - Start date (ISO format)' }); + placeholdersList.createEl('li', { text: '{end} - End date (ISO format)' }); + placeholdersList.createEl('li', { text: '{current} - Current date (ISO format)' }); + placeholdersList.createEl('li', { text: '{remaining} - Remaining time' }); + placeholdersList.createEl('li', { text: '{elapsed} - Elapsed time' }); + placeholdersList.createEl('li', { text: '{total} - Total duration (in days)' }); + + contentEl.createEl('h3', { text: 'Formatting' }); + contentEl.createEl('a', { href: 'https://moment.github.io/luxon/#/formatting?id=table-of-tokens', text: 'Luxon Formatting Reference' }); + + contentEl.createEl('h4', { text: 'Date formatting' }); + contentEl.createEl('p', { text: 'You can use Luxon formatting for dates (replace *format* with the format you want):' }); + const dateLuxonList = contentEl.createEl('ul'); + dateLuxonList.createEl('li', { text: '{start:*format*} - Format start date' }); + dateLuxonList.createEl('li', { text: '{end:*format*} - Format end date' }); + dateLuxonList.createEl('li', { text: '{current:*format*} - Format current date' }); + + contentEl.createEl('h5', { text: 'Examples' }); + const dateExamplesList = contentEl.createEl('ul'); + dateExamplesList.createEl('li', { text: '{start:LLL d, yyyy} - Start date' }); + dateExamplesList.createEl('li', { text: '{end:LLL d, yyyy} - End date' }); + dateExamplesList.createEl('li', { text: '{current:LLL d, yyyy} - Current date' }); + + + contentEl.createEl('h4', { text: 'Duration formatting' }); + contentEl.createEl('p', { text: 'For durations, the only tokens that are supported are for years, months, weeks, days, hours, minutes and seconds:' }); + const durationLuxonList = contentEl.createEl('ul'); + durationLuxonList.createEl('li', { text: '{remaining:*format*} - Format remaining time' }); + durationLuxonList.createEl('li', { text: '{elapsed:*format*} - Format elapsed time' }); + durationLuxonList.createEl('li', { text: '{total:*format*} - Format total duration' }); + + contentEl.createEl('h5', { text: 'Examples' }); + const durationExamplesList = contentEl.createEl('ul'); + durationExamplesList.createEl('li', { text: '{percent}% complete - {remaining} left' }); + durationExamplesList.createEl('li', { text: '{elapsed} elapsed out of {total} total' }); + + contentEl.createEl('h3', { text: 'Common Luxon Formats' }); + const formatsTable = contentEl.createEl('table'); + const headerRow = formatsTable.createEl('tr'); + headerRow.createEl('th', { text: 'Format' }); + headerRow.createEl('th', { text: 'Output' }); + + const addFormatRow = (format: string, output: string) => { + const row = formatsTable.createEl('tr'); + row.createEl('td', { text: format }); + row.createEl('td', { text: output }); + }; + + addFormatRow('yyyy-MM-dd', '2025-04-11'); + addFormatRow('LLL d, yyyy', 'Apr 11, 2025'); + addFormatRow('EEEE, MMMM d, yyyy', 'Thursday, April 11, 2025'); + addFormatRow('d MMMM yyyy', '11 April 2025'); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/obsidian.d.ts b/src/obsidian.d.ts new file mode 100644 index 0000000..c905a06 --- /dev/null +++ b/src/obsidian.d.ts @@ -0,0 +1,10 @@ +import "obsidian"; + +declare module "obsidian" { + interface Workspace { + on( + name: "countdown-to:rerender", + callback: () => void + ): EventRef; + } +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..3e99f0f --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,174 @@ +import { App, PluginSettingTab, Setting } from "obsidian"; +import CountdownToPlugin from "./main"; +import { LuxonFormatHelpModal } from "./modal"; + +export interface CountdownToSettings { + defaultBarColor: string; + defaultTrailColor: string; + defaultBarType: string; + defaultProgressType: string; + defaultOnCompleteText: string; + defaultInfoFormat: string; + defaultUpdateInRealTime: boolean; + defaultUpdateIntervalSeconds: number; +} + +export const DEFAULT_SETTINGS: CountdownToSettings = { + defaultBarColor: '#4CAF50', + defaultTrailColor: '#e0e0e0', + defaultBarType: 'Line', + defaultProgressType: 'Forward', + defaultOnCompleteText: '{title} is done!', + defaultInfoFormat: '{percent}% - {remaining} remaining', + defaultUpdateInRealTime: false, + defaultUpdateIntervalSeconds: 1, +}; + + +export class CountdownToSettingTab extends PluginSettingTab { + plugin: CountdownToPlugin; + + constructor( + app: App, + plugin: CountdownToPlugin + ) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl('h2', { text: 'Countdown To Settings' }); + + new Setting(containerEl).setName('Bar types').setHeading(); + new Setting(containerEl) + .setName('Default bar type') + .setDesc('Default type of progress bar to display') + .addDropdown(dropdown => dropdown + .addOption('Line', 'Line') + .addOption('Circle', 'Circle') + .addOption('SemiCircle', 'Semi Circle') + .addOption('Square', 'Square') + .setValue(this.plugin.settings.defaultBarType) + .onChange(async (value) => { + this.plugin.settings.defaultBarType = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })); + + new Setting(containerEl) + .setName('Default progress type') + .setDesc('Count as progress or as a countdown') + .addDropdown(dropdown => dropdown + .addOption('Progress', 'Progress') + .addOption('Countdown', 'Countdown') + .setValue(this.plugin.settings.defaultProgressType) + .onChange(async (value) => { + this.plugin.settings.defaultProgressType = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })); + + new Setting(containerEl).setName('Real time update').setHeading(); + new Setting(containerEl) + .setName('Update in real-time') + .setDesc('Update progress bars according to the update interval') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.defaultUpdateInRealTime) + .onChange(async (value) => { + this.plugin.settings.defaultUpdateInRealTime = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + this.display(); + })); + + if (this.plugin.settings.defaultUpdateInRealTime) { + new Setting(containerEl) + .setName('Update interval') + .setDesc('How often to update the progress bars (in seconds). This will affect performance.') + .addSlider(slider => slider + .setLimits(0.5, 20, 0.5) + .setValue(this.plugin.settings.defaultUpdateIntervalSeconds) + .setDynamicTooltip() + .onChange(async (value) => { + this.plugin.settings.defaultUpdateIntervalSeconds = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })); + } + + new Setting(containerEl).setName('Text').setHeading(); + new Setting(containerEl) + .setName('Default info format') + .setDesc('Default format for the info text. Uses Luxon formatting (See format help button for a quick reference).') + .addTextArea(text => text + .setPlaceholder('{percent}% - {remaining} remaining') + .setValue(this.plugin.settings.defaultInfoFormat) + .onChange(async (value) => { + this.plugin.settings.defaultInfoFormat = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })) + .addExtraButton(button => { + button + .setIcon('help') + .setTooltip('Show format help') + .onClick(() => { + new LuxonFormatHelpModal(this.plugin.app).open(); + }); + }); + + new Setting(containerEl) + .setName('Default on complete text') + .setDesc('Default text to display when the progress is complete. Use {title} to display the title of the progress bar.') + .addText(text => text + .setPlaceholder('{title} is done!') + .setValue(this.plugin.settings.defaultOnCompleteText) + .onChange(async (value) => { + this.plugin.settings.defaultOnCompleteText = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })); + + new Setting(containerEl).setName('Colors').setHeading(); + new Setting(containerEl) + .setName('Default bar color') + .setDesc('Default color for the progress bar') + .addColorPicker(color => color + .setValue(this.plugin.settings.defaultBarColor) + .onChange(async (value) => { + this.plugin.settings.defaultBarColor = value; + await this.plugin.saveSettings(); + this.app.workspace.trigger( + "countdown-to:rerender" + ); + })); + + new Setting(containerEl) + .setName('Default trail color') + .setDesc('Default trail color for the progress bar (the incomplete part)') + .addColorPicker(color => color + .setValue(this.plugin.settings.defaultTrailColor) + .onChange(async (value) => { + this.plugin.settings.defaultTrailColor = value; + await this.plugin.saveSettings(); + + })); + containerEl.createEl('br'); + containerEl.createEl('i', { text: 'All settings can be overridden in the markdown code block. If stuck please refer to the ' }); + containerEl.createEl('a', { href: 'https://github.com/guicattani/countdown-to?tab=readme-ov-file#how-to-use', text: 'how to use guide' }); + } +} From f1a3c2233c9471d4184d3234667af7c60cc2d885 Mon Sep 17 00:00:00 2001 From: Guilherme Cattani Date: Wed, 26 Mar 2025 20:55:43 +0100 Subject: [PATCH 4/4] Version bump to 1.2.0 --- manifest.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 14ea7fc..b21e1ec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "countdown-to", "name": "Countdown To", - "version": "1.1.0", + "version": "1.2.0", "minAppVersion": "0.15.0", "description": "Create countdown progress bars in your notes", "author": "Gui Cattani", diff --git a/package.json b/package.json index bfefae1..2792c07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-countdown-to", - "version": "1.0.0", + "version": "1.2.0", "description": "Create countdown progress bars in your notes", "main": "main.js", "scripts": {