mirror of
https://github.com/guicattani/obsidian-countdown-to.git
synced 2026-07-22 05:42:39 +00:00
Merge pull request #1 from guicattani/fix-pre-release-bugs
Fix pre release bugs
This commit is contained in:
commit
817b319585
11 changed files with 590 additions and 520 deletions
|
|
@ -33,17 +33,17 @@ 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
|
||||
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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const context = await esbuild.context({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
|
|
|
|||
506
main.ts
506
main.ts
|
|
@ -1,506 +0,0 @@
|
|||
import { App, Plugin, PluginSettingTab, Setting, Modal } from 'obsidian';
|
||||
import { DateTime, Duration, Interval } from 'luxon';
|
||||
import * as ProgressBar from 'progressbar.js';
|
||||
|
||||
interface ProgressBarSettings {
|
||||
defaultBarColor: string;
|
||||
defaultTrailColor: string;
|
||||
defaultBarType: string;
|
||||
defaultProgressType: string;
|
||||
defaultOnCompleteText: string;
|
||||
defaultInfoFormat: string;
|
||||
defaultUpdateInRealTime: boolean;
|
||||
defaultUpdateIntervalSeconds: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: ProgressBarSettings = {
|
||||
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 ProgressBarJs {
|
||||
set(progress: number): void;
|
||||
}
|
||||
|
||||
interface ProgressBarInstance {
|
||||
element: HTMLElement;
|
||||
bar: ProgressBarJs;
|
||||
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 ProgressBarSettingTab extends PluginSettingTab {
|
||||
plugin: ProgressBarPlugin;
|
||||
|
||||
constructor(app: App, plugin: ProgressBarPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl('h2', { text: 'Countdown To Settings' });
|
||||
|
||||
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('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('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('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 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')
|
||||
.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('Default trail color')
|
||||
.setDesc('Default trail color for the progress bar (the incomplete part)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('#e0e0e0')
|
||||
.setValue(this.plugin.settings.defaultTrailColor)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultTrailColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
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 ProgressBarPlugin extends Plugin {
|
||||
settings: ProgressBarSettings;
|
||||
|
||||
progressBars = new Map<string, ProgressBarInstance>();
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor('progressbar', (source, el) => {
|
||||
const id = Math.random().toString(36).substring(2, 15);
|
||||
this.renderProgressBar(source, el, id);
|
||||
});
|
||||
|
||||
this.addSettingTab(new ProgressBarSettingTab(this.app, this));
|
||||
this.register(() => {
|
||||
this.cleanupAllProgressBars();
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.cleanupAllProgressBars();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.refreshAllProgressBars();
|
||||
}
|
||||
|
||||
cleanupProgressBar(id: string) {
|
||||
const progressBar = this.progressBars.get(id);
|
||||
if (progressBar && progressBar.updateTimer) {
|
||||
window.clearTimeout(progressBar.updateTimer);
|
||||
this.progressBars.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
cleanupAllProgressBars() {
|
||||
this.progressBars.forEach((data) => {
|
||||
if (data.updateTimer) {
|
||||
window.clearTimeout(data.updateTimer);
|
||||
}
|
||||
});
|
||||
this.progressBars.clear();
|
||||
}
|
||||
|
||||
refreshAllProgressBars() {
|
||||
this.progressBars.forEach((data, id) => {
|
||||
if (data.updateTimer) {
|
||||
window.clearTimeout(data.updateTimer);
|
||||
data.updateTimer = null;
|
||||
}
|
||||
|
||||
this.renderProgressBar(data.params, data.element, id);
|
||||
});
|
||||
}
|
||||
|
||||
renderProgressBar(source: string, el: HTMLElement, id: string) {
|
||||
try {
|
||||
this.cleanupProgressBar(id);
|
||||
|
||||
const params = this.parseProgressBarParams(source);
|
||||
|
||||
el.empty();
|
||||
const containerEl = el.createDiv({ cls: '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 progressBarEl = containerEl.createDiv({ cls: 'progress-bar-element' });
|
||||
const barType = params.type || this.settings.defaultBarType;
|
||||
progressBarEl.addClass(`progress-bar-${barType.toLowerCase()}`);
|
||||
|
||||
const infoEl = containerEl.createDiv({ cls: 'progress-bar-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(progressBarEl, {
|
||||
...commonOptions,
|
||||
svgStyle: { width: '100%', height: '100%' },
|
||||
});
|
||||
break;
|
||||
case 'semicircle':
|
||||
bar = new ProgressBar.SemiCircle(progressBarEl, {
|
||||
...commonOptions,
|
||||
svgStyle: { width: '100%', height: '100%' },
|
||||
});
|
||||
break;
|
||||
case 'square':
|
||||
bar = new ProgressBar.Square(progressBarEl, {
|
||||
...commonOptions,
|
||||
svgStyle: { width: '100%', height: '100%' },
|
||||
});
|
||||
break;
|
||||
case 'line':
|
||||
default:
|
||||
bar = new ProgressBar.Line(progressBarEl, {
|
||||
...commonOptions,
|
||||
svgStyle: { width: '100%', height: '100%' },
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (params.title) {
|
||||
const titleEl = containerEl.createDiv({ cls: 'progress-bar-title' });
|
||||
titleEl.setText(params.title);
|
||||
containerEl.prepend(titleEl);
|
||||
}
|
||||
|
||||
this.progressBars.set(id, {
|
||||
element: el,
|
||||
bar: bar,
|
||||
infoEl: infoEl,
|
||||
params: source,
|
||||
updateTimer: null,
|
||||
});
|
||||
|
||||
this.updateProgressBar(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 progressBarInstance = this.progressBars.get(id);
|
||||
if (progressBarInstance) {
|
||||
progressBarInstance.updateTimer = timer;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
el.setText('Error rendering progress bar: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUpdate(id: string, startDate: DateTime, endDate: DateTime, defaultUpdateIntervalSeconds: number) {
|
||||
const progressBar = this.progressBars.get(id);
|
||||
if (!progressBar) return;
|
||||
|
||||
this.updateProgressBar(id, startDate, endDate);
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
this.scheduleUpdate(id, startDate, endDate, defaultUpdateIntervalSeconds);
|
||||
}, defaultUpdateIntervalSeconds * 1000);
|
||||
|
||||
progressBar.updateTimer = timer;
|
||||
}
|
||||
|
||||
updateProgressBar(id: string, startDate: DateTime, endDate: DateTime) {
|
||||
const progressBar = this.progressBars.get(id);
|
||||
if (!progressBar) return;
|
||||
|
||||
const params = this.parseProgressBarParams(progressBar.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') {
|
||||
progressBar.bar.set(1.0 - progress);
|
||||
} else {
|
||||
progressBar.bar.set(Math.floor(progress * 100) / 100);
|
||||
}
|
||||
|
||||
if (progress >= 1) {
|
||||
progressBar.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));
|
||||
|
||||
progressBar.infoEl.setText(infoText);
|
||||
}
|
||||
}
|
||||
|
||||
parseProgressBarParams(source: string): Record<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
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' : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
262
src/countdownToMarkdownRenderChild.ts
Normal file
262
src/countdownToMarkdownRenderChild.ts
Normal file
|
|
@ -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<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
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' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
61
src/main.ts
Normal file
61
src/main.ts
Normal file
|
|
@ -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<string, CountdownToInstace>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
70
src/modal.ts
Normal file
70
src/modal.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
10
src/obsidian.d.ts
vendored
Normal file
10
src/obsidian.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import "obsidian";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
on(
|
||||
name: "countdown-to:rerender",
|
||||
callback: () => void
|
||||
): EventRef;
|
||||
}
|
||||
}
|
||||
174
src/settings.ts
Normal file
174
src/settings.ts
Normal file
|
|
@ -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' });
|
||||
}
|
||||
}
|
||||
15
styles.css
15
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 .countdown-to-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-bar-info {
|
||||
.countdown-to-plugin .countdown-to-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 .countdown-to-circle,
|
||||
.countdown-to-plugin .countdown-to-semicircle,
|
||||
.countdown-to-plugin .countdown-to-square {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
/* Line can stay with the default height */
|
||||
.progress-bar-line {
|
||||
.countdown-to-plugin .countdown-to-line {
|
||||
height: 20px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue