mirror of
https://github.com/guicattani/obsidian-countdown-to.git
synced 2026-07-22 05:42:39 +00:00
Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad7db0c5c | ||
|
|
95aeca4ecf | ||
|
|
d4c9364d4d | ||
|
|
10e73139d6 | ||
|
|
7981cbd273 | ||
|
|
1855612876 | ||
|
|
1aa146924a | ||
|
|
0246016b48 | ||
|
|
f14e8b514d | ||
|
|
e6f1a0dffb | ||
|
|
874e7ce377 | ||
|
|
d91f19eb3d | ||
|
|
eb3f990fd9 |
11 changed files with 440 additions and 86 deletions
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
60
README.md
60
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue