Compare commits

..

13 commits
1.3.0 ... main

Author SHA1 Message Date
Guilherme Cattani
7ad7db0c5c Bump version to 1.6.0 2026-05-20 09:31:11 +02:00
Guilherme Cattani
95aeca4ecf
Merge pull request #8 from ysunnn/main
Add dynamic gradient coloring based on progress
2026-05-19 09:41:56 +00:00
Yousun
d4c9364d4d feat(gradient): Implement 2/3-point color gradients on progress bars 2025-11-01 19:01:44 +01:00
Guilherme Cattani
10e73139d6 Add version bump 2025-08-31 15:11:57 +02:00
Guilherme Cattani
7981cbd273
Merge pull request #7 from guicattani/add-properties
Add properties support
2025-08-31 15:08:31 +02:00
Guilherme Cattani
1855612876 Add properties support 2025-08-31 15:07:49 +02:00
Guilherme Cattani
1aa146924a
Merge pull request #3 from guicattani/add-start-date-in-future-plus-fixes
Add start date in future
Add new line support
Bug fixes
2025-06-28 13:44:43 +02:00
Guilherme Cattani
0246016b48 Add new line support, bugfixes
Fixed a bug where the container was duplicated on error
Removed the default background for upcoming since it can't adhere to the
user theme
2025-06-28 13:39:07 +02:00
Guilherme Cattani
f14e8b514d
Update issue templates 2025-06-28 13:24:13 +02:00
Guilherme Cattani
e6f1a0dffb Add different background to upcoming countdowns
Add the settings to change the background of upcoming countdowns
2025-06-22 00:21:05 +02:00
Guilherme Cattani
874e7ce377 Add start date in the future
Add start date in the future feature
Fix remaining logic being wrong for using ceil()
Improved formatDuration running even for unexistent tags
Improved Readme with new info and CSS customization section
Improved error message not having a container and looking weird for some
error messages
2025-06-22 00:10:48 +02:00
Guilherme Cattani
d91f19eb3d Use sentence case where missing 2025-06-10 22:31:03 +02:00
Guilherme Cattani
eb3f990fd9 Fix casing on modal, remove header from settings 2025-04-06 19:40:53 +02:00
11 changed files with 440 additions and 86 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] Bug title"
labels: bug
assignees: guicattani
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop or Smartphone Obsidian (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE REQUEST] New feature"
labels: enhancement
assignees: guicattani
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -34,25 +34,34 @@ 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 `countdown-to` language identifier:
````markdown
````
```countdown-to
title: Project Deadline
startDate: 2025-03-12T08:00:00
endDate: 2025-04-11T14:00:00
type: Circle
startDate: 2025-03-12
startTime: 08:00:00
endDate: 2025-04-11
endTime: 14:00:00
type: circle
color: #ff5722
trailColor: #f5f5f5
infoFormat: {percent}% complete - {remaining} until {end:LLL d, yyyy}
updateInRealTime: true
updateInterval: 30
updateIntervalInSeconds: 30
```
````
### Required Parameters
- `endDate`: The target date to count down to (ISO format: YYYY-MM-DDTHH:MM:SS)
- `startDate`: Start date for the progress calculation, needs to be now or in the past (ISO format: YYYY-MM-DDTHH:MM:SS)
If no startDate and endDate is provided the countdown will assume the current day. So at the very least the widget requires a start time and end time. If that is the case the countdown will run daily.
- `startDate`: Start date for the progress calculation (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
- `startTime`: Start time for the progress calculation (format: HH:MM:SS, optional - defaults to 00:00:00)
- `endDate`: The target date to count down to (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
- `endTime`: The target time to count down to (format: HH:MM:SS, optional - defaults to 00:00:00)
**Note**: You can use either:
- Separate date and time parameters: `startDate: 2025-01-01` + `startTime: 09:00:00`
- Combined ISO format: `startDate: 2025-01-01T09:00:00` startTime will be ignored. This logic is the same for endDate and endTime
- The startDate or startTime can be in the future, and the info message will reflect that it is upcoming. The message for upcoming countdowns can be customized in the settings under `Default info format upcoming`.
### Optional Parameters
@ -64,7 +73,7 @@ updateInterval: 30
- `onCompleteText`: Text to display when the countdown is complete
- `infoFormat`: Custom format for the information text
- `updateInRealTime`: Whether to update the progress bar in real-time (`true`/`false`)
- `updateInterval`: How often to update the progress bar (in seconds)
- `updateIntervalInSeconds`: How often to update the progress bar
## Formatting Options
@ -97,6 +106,34 @@ For example:
See the [Luxon formatting reference](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) for all available format tokens.
### Using parameters
Countdown To is compatible with Obsidian Parameters. To use them you use the same parameters explained in #How to Use but have them prefixed with `countdown-`, for example:
```
---
countdown-startDate: 2025-08-31
countdown-startTime: 19:00
countdown-endDate: 2025-09-15
countdown-endTime: 19:00
countdown-title: Property Title Test
countdown-color: "#ff5722"
countdown-type: Line
countdown-trailColor: "#ff0022"
countdown-progressType: progress
countdown-updateInRealTime: true
countdown-updateIntervalInSeconds: 10
countdown-infoFormat: "{percent}% complete - {remaining} until {end:LLL d, yyyy}"
countdown-infoFormatUpcoming: Upcoming {title}
countdown-onCompleteText: Completed {title}
---
```
These parameters can be overriden in a `countdown-to` but they will now be the default value for the file and they don't need to be written in a `countdown-to` block.
````
```countdown-to
```
````
Will now work even with no `startDate`
## Configuration
You can configure default settings for all progress bars in the plugin settings:
@ -107,6 +144,9 @@ You can configure default settings for all progress bars in the plugin settings:
All settings can be overridden in individual progress bar code blocks.
## CSS Customization
A custom CSS snippet can be created in your `./.obsidian/snippets` and the classes on https://github.com/guicattani/obsidian-countdown-to/blob/main/styles.css can be overriden.
## Performance Considerations
- Setting a very low update interval (e.g., 1 second) for many progress bars may impact performance

View file

@ -1,10 +1,10 @@
{
"id": "countdown-to",
"name": "Countdown To",
"version": "1.3.0",
"version": "1.6.0",
"minAppVersion": "0.15.0",
"description": "Create countdown progress bars in your notes",
"author": "Gui Cattani",
"authorUrl": "https://github.com/guicattani/countdown-to",
"authorUrl": "https://github.com/guicattani/",
"isDesktopOnly": false
}

4
package-lock.json generated
View file

@ -1707,8 +1707,8 @@
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.5.0.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-countdown-to",
"version": "1.3.0",
"version": "1.5.0",
"description": "Create countdown progress bars in your notes",
"main": "main.js",
"scripts": {

View file

@ -1,4 +1,4 @@
import { MarkdownRenderChild } from "obsidian";
import { MarkdownRenderChild, MarkdownPostProcessorContext, TFile } from "obsidian";
import CountdownToPlugin from "./main";
import { DateTime, Duration, Interval } from 'luxon';
@ -8,16 +8,19 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
plugin: CountdownToPlugin;
source: string;
id: string;
context: MarkdownPostProcessorContext;
constructor(
plugin: CountdownToPlugin,
source: string,
containerEl: HTMLElement,
id: string
id: string,
context: MarkdownPostProcessorContext
) {
super(containerEl);
this.plugin = plugin;
this.id = id;
this.source = source;
this.context = context;
this.display();
}
@ -49,26 +52,25 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
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;
}
const startDate = this.constructDateTime(params.startDate, params.startTime, 'start');
const endDate = this.constructDateTime(params.endDate, params.endTime, 'end');
if (endDate < startDate) {
containerEl.setText('End date must be after start date.');
containerEl.setText('End date/time must be after start date/time.');
return;
}
// Apply upcoming background color if start date is in the future
const isUpcoming = startDate > DateTime.now();
if (isUpcoming) {
containerEl.addClass('countdown-to-upcoming');
document.documentElement.style.setProperty(
'--countdown-to-upcoming-bg',
this.plugin.settings.defaultUpcomingBackgroundColor
);
}
const countdownToEl = containerEl.createDiv({ cls: 'countdown-to-element' });
const barType = params.type || this.plugin.settings.defaultBarType;
countdownToEl.addClass(`countdown-to-${barType.toLowerCase()}`);
@ -76,7 +78,18 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
const infoEl = containerEl.createDiv({ cls: 'countdown-to-info' });
let bar;
const barColor = params.color || this.plugin.settings.defaultBarColor;
const initialbarColor = params.color || this.plugin.settings.defaultBarColor;
const isGradient = (params.colorInGradient || this.plugin.settings.defaultColorInGradient.toString()) === 'true';
const progressType = params.progressType || this.plugin.settings.defaultProgressType;
let barColor: string;
if (isGradient) {
const startColorParam = params.startColor || this.plugin.settings.defaultStartColor;
const endColorParam = params.endColor || this.plugin.settings.defaultEndColor;
const isCountdown = progressType.toLowerCase() === 'countdown';
barColor = isCountdown ? endColorParam : startColorParam;
} else {
barColor = initialbarColor;
}
const trailColor = params.trailColor || this.plugin.settings.defaultTrailColor;
const commonOptions = {
strokeWidth: 4,
@ -115,7 +128,12 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
if (params.title) {
const titleEl = containerEl.createDiv({ cls: 'countdown-to-title' });
titleEl.setText(params.title);
const titleLines = params.title.split('\\n');
titleLines.forEach(line => {
const lineEl = titleEl.createDiv({ cls: 'countdown-to-title-line' });
lineEl.setText(line);
});
containerEl.prepend(titleEl);
}
@ -125,23 +143,35 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
infoEl: infoEl,
params: this.source,
updateTimer: null,
startedAsUpcoming: isUpcoming,
});
this.updateCountdownTo(this.id, startDate, endDate);
if (startDate > DateTime.now()) {
this.updateCountdownTo(this.id, DateTime.now(), startDate, true);
} else {
this.updateCountdownTo(this.id, startDate, endDate, false);
}
const updateInRealTime = params.updateInRealTime !== undefined ?
params.updateInRealTime === 'true' :
this.plugin.settings.defaultUpdateInRealTime;
if (updateInRealTime) {
const updateInterval = params.updateInterval ?
parseInt(params.updateInterval, 10) :
if (params.updateInterval && !params.updateIntervalInSeconds) {
params.updateIntervalInSeconds = params.updateInterval;
}
const updateIntervalInSeconds = params.updateIntervalInSeconds ?
parseInt(params.updateIntervalInSeconds, 10) :
this.plugin.settings.defaultUpdateIntervalSeconds;
this.scheduleUpdate(this.id, startDate, endDate, updateInterval);
this.scheduleUpdate(this.id, startDate, endDate, updateIntervalInSeconds);
}
} catch (error) {
this.containerEl.setText('Error rendering countdown to: ' + error.message);
this.containerEl.empty();
const containerEl = this.containerEl.createDiv({ cls: ['countdown-to-plugin', 'countdown-to-container'] });
containerEl.setText('Error rendering countdown to: ' + error.message);
}
}
@ -150,19 +180,30 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
if (!countdownTo) return;
const timer = window.setInterval(() => {
this.updateCountdownTo(id, startDate, endDate);
if (startDate > DateTime.now()) {
this.updateCountdownTo(this.id, DateTime.now(), startDate, true);
} else {
this.updateCountdownTo(this.id, startDate, endDate, false);
}
}, defaultUpdateIntervalSeconds * 1000);
countdownTo.updateTimer = timer;
}
updateCountdownTo(id: string, startDate: DateTime, endDate: DateTime) {
updateCountdownTo(id: string, startDate: DateTime, endDate: DateTime, upcoming: boolean = false) {
const countdownTo = this.plugin.countdownTos.get(id);
if (!countdownTo) return;
const params = this.parseCountdownToParams(countdownTo.params);
const currentDate = DateTime.now();
if (countdownTo.startedAsUpcoming) {
if (!upcoming) {
const containerEl = countdownTo.element.querySelector('.countdown-to-container') as HTMLElement;
containerEl.removeClass('countdown-to-upcoming');
}
}
const totalInterval = Interval.fromDateTimes(startDate, endDate);
const elapsedInterval = Interval.fromDateTimes(startDate, currentDate);
@ -173,19 +214,28 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
const progressType = params.progressType || this.plugin.settings.defaultProgressType;
const onCompleteText = params.onCompleteText || this.plugin.settings.defaultOnCompleteText;
const infoFormat = params.infoFormat || this.plugin.settings.defaultInfoFormat;
const infoFormatUpcoming = params.infoFormatUpcoming || this.plugin.settings.defaultInfoFormatUpcoming;
if (progressType.toLowerCase() === 'countdown') {
countdownTo.bar.set(1.0 - progress);
} else {
countdownTo.bar.set(Math.floor(progress * 100) / 100);
}
this.updateBarGradient(countdownTo, progress, params, progressType);
if (progress >= 1) {
countdownTo.infoEl.setText(
onCompleteText.replace(/{title}/g, params.title || ''),
);
} else {
let infoText = infoFormat;
let infoText;
if (upcoming) {
infoText = infoFormatUpcoming;
} else {
infoText = infoFormat;
}
const remainingInterval = Interval.fromDateTimes(currentDate, endDate);
const remainingDuration = remainingInterval.toDuration(['days', 'hours', 'minutes', 'seconds']);
@ -193,6 +243,7 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
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))
@ -206,18 +257,137 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
.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));
.replace(/{title}/g, params.title || '');
countdownTo.infoEl.setText(infoText);
if (infoText.includes('{remaining}')) {
infoText = infoText.replace(/{remaining}/g, this.formatDuration(remainingDuration))
}
if (infoText.includes('{elapsed}')) {
infoText = infoText.replace(/{elapsed}/g, this.formatDuration(elapsedDuration))
}
if (infoText.includes('{total}')) {
infoText = infoText.replace(/{total}/g, this.formatDuration(totalDuration))
}
countdownTo.infoEl.empty();
const infoLines = infoText.split('\\n');
infoLines.forEach(line => {
const lineEl = countdownTo.infoEl.createDiv({ cls: 'countdown-to-info-line' });
lineEl.setText(line);
});
}
}
parseCountdownToParams(source: string): Record<string, string> {
const params: Record<string, string> = {};
const lines = source.trim().split('\n');
private updateBarGradient(
countdownTo: any,
progress: number,
params: Record<string, string>,
progressType: string
): void {
const isGradient =
(params.colorInGradient || this.plugin.settings.defaultColorInGradient?.toString()) === "true";
if (!isGradient || !countdownTo?.bar) return;
const isCountdown = progressType.toLowerCase() === "countdown";
const startColor = params.startColor || this.plugin.settings.defaultStartColor;
const endColor = params.endColor || this.plugin.settings.defaultEndColor;
const midColorParam = params.midColor || this.plugin.settings.defaultMidColor;
const midColor = this.parseHexColor(midColorParam);
let colorProgress: number;
if (isCountdown) {
colorProgress = 1.0 - progress;
} else {
colorProgress = Math.floor(progress * 100) / 100;
}
let newColor: string;
if (midColor) {
if (colorProgress < 0.5) {
newColor = this.interpolateColor(startColor, midColorParam, colorProgress * 2);
} else {
newColor = this.interpolateColor(midColorParam, endColor, (colorProgress - 0.5) * 2);
}
} else {
newColor = this.interpolateColor(startColor, endColor, colorProgress);
}
const barPath = (countdownTo.bar as any)?.path as SVGPathElement;
if (barPath) barPath.setAttribute("stroke", newColor);
}
private interpolateColor(color1: string, color2: string, factor: number): string {
const c1 = this.parseHexColor(color1);
const c2 = this.parseHexColor(color2);
if (!c1 || !c2) return color1;
const result = c1.map((v, i) => Math.round(v + factor * (c2[i] - v)));
return `#${result.map((x) => x.toString(16).padStart(2, "0")).join("")}`;
}
private parseHexColor(hex: string): number[] | null {
if (!hex) return null;
const match = hex.trim().replace("#", "").match(/^([0-9a-f]{6})$/i);
if (!match) return null;
const intVal = parseInt(match[1], 16);
return [(intVal >> 16) & 255, (intVal >> 8) & 255, intVal & 255];
}
getPropertiesFromFrontmatter(): Record<string, string> {
const properties: Record<string, string> = {};
if (!this.context.sourcePath) {
return properties;
}
const file = this.plugin.app.vault.getAbstractFileByPath(this.context.sourcePath);
if (!file || !(file instanceof TFile)) {
return properties;
}
const fileCache = this.plugin.app.metadataCache.getFileCache(file);
if (!fileCache || !fileCache.frontmatter) {
return properties;
}
const frontmatter = fileCache.frontmatter;
const propertyMapping = [
'startDate',
'startTime',
'endDate',
'endTime',
'title',
'color',
'trailColor',
'type',
'progressType',
'updateInRealTime',
'updateInterval',
'updateIntervalInSeconds',
'infoFormat',
'infoFormatUpcoming',
'onCompleteText',
'colorInGradient',
'startColor',
'endColor',
'midColor'
];
propertyMapping.forEach(prop => {
const prefixedKey = `countdown-${prop}`;
if (frontmatter[prefixedKey] !== undefined) {
properties[prop] = String(frontmatter[prefixedKey]);
}
});
return properties;
}
parseCountdownToParams(source: string): Record<string, string> {
// Start with properties from frontmatter (obsidian metadata) and override with code block parameters
const params: Record<string, string> = this.getPropertiesFromFrontmatter();
const lines = source.trim().split('\n');
lines.forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex !== -1) {
@ -229,36 +399,72 @@ export class CountdownToMarkdownRenderChild extends MarkdownRenderChild {
}
});
if (!params.startDate) {
throw new Error('Start date is required');
if (!params.startDate && !params.startTime) {
throw new Error('Start date or start time is required');
}
if (DateTime.fromISO(params.startDate) > DateTime.now()) {
throw new Error('Start date must be now or in the past');
if (!params.endDate && !params.endTime) {
throw new Error('End date or end time is required');
}
if (!params.endDate) {
throw new Error('End date is required');
if (params.startTime && !params.startDate && params.endDate && !params.endTime ||
params.startTime && !params.startDate && params.endDate && params.endTime) {
throw new Error('Start time with no start date requires an end time with no end date');
}
if (params.startDate === params.endDate && params.startTime === params.endTime) {
throw new Error('Start date and end date cannot be the same');
}
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);
const days = Math.floor(duration.as('days'));
const hours = Math.floor(duration.as('hours') % 24);
const minutes = Math.floor(duration.as('minutes') % 60);
const seconds = Math.floor(duration.as('seconds') % 60);
if (days > 0) {
return `${days} day${days > 1 ? 's' : ''}`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''}`;
return `${hours} hour${hours > 1 ? 's' : ''} ` +
`${minutes} minute${minutes > 1 ? 's' : ''} ` +
`${seconds} second${seconds > 1 ? 's' : ''}`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
return `${minutes} minute${minutes > 1 ? 's' : ''} ` +
`${seconds} second${seconds > 1 ? 's' : ''}`;
} else {
return `${seconds} second${seconds > 1 ? 's' : ''}`;
}
}
constructDateTime(date: string, time: string, type: string): DateTime {
const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/;
if (isoDateTimeRegex.test(date)) {
const dateTime = DateTime.fromISO(date);
return dateTime;
}
let dateToUse = date || DateTime.now().toFormat('yyyy-MM-dd');
let timeToUse = time || '00:00:00';
if (timeToUse && !timeToUse.includes(':')) {
timeToUse = `${timeToUse}:00`;
} else if (timeToUse && timeToUse.split(':').length === 2) {
timeToUse = `${timeToUse}:00`;
}
const dateTimeString = `${dateToUse}T${timeToUse}`;
const dateTime = DateTime.fromISO(dateTimeString);
if (!dateTime.isValid) {
throw new Error(`Invalid ${type} date or time format: ${dateTimeString}`);
}
return dateTime;
}
}

View file

@ -2,22 +2,22 @@ import { Plugin } from 'obsidian';
import { CountdownToSettings, CountdownToSettingTab, DEFAULT_SETTINGS } from './settings';
import { CountdownToMarkdownRenderChild } from './countdownToMarkdownRenderChild';
// Minimal interface for progressbar.js instances
interface CountdownToJs {
interface ProgressBarJS {
set(progress: number): void;
}
interface CountdownToInstace {
interface CountdownToInstance {
element: HTMLElement;
bar: CountdownToJs;
bar: ProgressBarJS;
infoEl: HTMLElement;
params: string;
updateTimer: number | null;
startedAsUpcoming: boolean;
}
export default class CountdownToPlugin extends Plugin {
settings: CountdownToSettings;
countdownTos = new Map<string, CountdownToInstace>();
countdownTos = new Map<string, CountdownToInstance>();
async onload() {
await this.loadSettings();
@ -28,7 +28,7 @@ export default class CountdownToPlugin extends Plugin {
id = Math.random().toString(36).substring(2, 15);
}
ctx.addChild(new CountdownToMarkdownRenderChild(this, source, el, id));
ctx.addChild(new CountdownToMarkdownRenderChild(this, source, el, id, ctx));
});
this.addSettingTab(new CountdownToSettingTab(this.app, this));

View file

@ -4,7 +4,7 @@ export class LuxonFormatHelpModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Info Format Help' });
contentEl.createEl('h2', { text: 'Info format help' });
contentEl.createEl('h3', { text: 'Placeholders' });
const placeholdersList = contentEl.createEl('ul');
@ -17,7 +17,7 @@ export class LuxonFormatHelpModal extends Modal {
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('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):' });
@ -45,7 +45,7 @@ export class LuxonFormatHelpModal extends Modal {
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' });
contentEl.createEl('h3', { text: 'Common Luxon formats' });
const formatsTable = contentEl.createEl('table');
const headerRow = formatsTable.createEl('tr');
headerRow.createEl('th', { text: 'Format' });

View file

@ -5,26 +5,37 @@ import { LuxonFormatHelpModal } from "./modal";
export interface CountdownToSettings {
defaultBarColor: string;
defaultTrailColor: string;
defaultUpcomingBackgroundColor: string;
defaultBarType: string;
defaultProgressType: string;
defaultOnCompleteText: string;
defaultInfoFormat: string;
defaultInfoFormatUpcoming: string;
defaultUpdateInRealTime: boolean;
defaultUpdateIntervalSeconds: number;
defaultColorInGradient: boolean;
defaultStartColor: string;
defaultEndColor: string;
defaultMidColor: string;
}
export const DEFAULT_SETTINGS: CountdownToSettings = {
defaultBarColor: '#4CAF50',
defaultTrailColor: '#e0e0e0',
defaultUpcomingBackgroundColor: '#gggggg', // invalid color so that we know the user didn't set it
defaultBarType: 'Line',
defaultProgressType: 'Forward',
defaultOnCompleteText: '{title} is done!',
defaultInfoFormat: '{percent}% - {remaining} remaining',
defaultInfoFormatUpcoming: '{title} is coming up in {remaining}!',
defaultUpdateInRealTime: false,
defaultUpdateIntervalSeconds: 1,
defaultColorInGradient: false,
defaultStartColor: '#ff5722',
defaultEndColor: '#4CAF50',
defaultMidColor: ' ',
};
export class CountdownToSettingTab extends PluginSettingTab {
plugin: CountdownToPlugin;
@ -39,7 +50,6 @@ export class CountdownToSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Countdown To Settings' });
new Setting(containerEl).setName('Bar types').setHeading();
new Setting(containerEl)
@ -103,7 +113,8 @@ export class CountdownToSettingTab extends PluginSettingTab {
this.app.workspace.trigger(
"countdown-to:rerender"
);
}));
})
);
}
new Setting(containerEl).setName('Text').setHeading();
@ -116,10 +127,34 @@ export class CountdownToSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.defaultInfoFormat = value;
await this.plugin.saveSettings();
this.app.workspace.trigger(
"countdown-to:rerender"
);
}))
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 info format upcoming')
.setDesc('Default format for the info text when the countdown is upcoming (start date in the future).')
.addTextArea(text => text
.setPlaceholder('{title} is coming up in {remaining}!')
.setValue(this.plugin.settings.defaultInfoFormatUpcoming)
.onChange(async (value) => {
this.plugin.settings.defaultInfoFormatUpcoming = value;
await this.plugin.saveSettings();
this.app.workspace.trigger(
"countdown-to:rerender"
);
})
)
.addExtraButton(button => {
button
.setIcon('help')
@ -138,10 +173,11 @@ export class CountdownToSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.defaultOnCompleteText = value;
await this.plugin.saveSettings();
this.app.workspace.trigger(
"countdown-to:rerender"
);
}));
this.app.workspace.trigger(
"countdown-to:rerender"
);
})
);
new Setting(containerEl).setName('Colors').setHeading();
new Setting(containerEl)
@ -152,10 +188,11 @@ export class CountdownToSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.defaultBarColor = value;
await this.plugin.saveSettings();
this.app.workspace.trigger(
"countdown-to:rerender"
);
}));
this.app.workspace.trigger(
"countdown-to:rerender"
);
})
);
new Setting(containerEl)
.setName('Default trail color')
@ -165,8 +202,23 @@ export class CountdownToSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.defaultTrailColor = value;
await this.plugin.saveSettings();
this.app.workspace.trigger(
"countdown-to:rerender"
);
})
);
new Setting(containerEl)
.setName('Default upcoming background color')
.setDesc('Default background color for countdowns that are upcoming (start date in the future). Needs manual reload of notes to take effect.')
.addColorPicker(color => color
.setValue(this.plugin.settings.defaultUpcomingBackgroundColor)
.onChange(async (value) => {
this.plugin.settings.defaultUpcomingBackgroundColor = 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' });

View file

@ -3,6 +3,11 @@
padding: 10px;
border-radius: 5px;
background-color: var(--background-secondary);
transition: background-color 1.0s ease;
}
.countdown-to-plugin.countdown-to-container.countdown-to-upcoming {
background-color: var(--countdown-to-upcoming-bg, var(--background-secondary, #e3e3e3));
}
.countdown-to-plugin .countdown-to-title {