switch to ical.js - v4.2

This commit is contained in:
2026-02-21 15:31:22 -08:00
parent e44a538822
commit a096b8d18f
4 changed files with 56 additions and 24 deletions

View File

@@ -1,6 +1,6 @@
--- ---
name: Library/sstent/icalendar name: Library/sstent/icalendar
version: "0.4.1" version: "0.4.2"
tags: meta/library tags: meta/library
files: files:
- icalendar.plug.js - icalendar.plug.js

View File

@@ -1,6 +1,6 @@
{ {
"name": "icalendar-plug", "name": "icalendar-plug",
"version": "0.4.1", "version": "0.4.2",
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"tasks": { "tasks": {
"sync-version": "deno run -A scripts/sync-version.ts", "sync-version": "deno run -A scripts/sync-version.ts",

View File

@@ -1,5 +1,5 @@
name: icalendar name: icalendar
version: 0.4.1 version: 0.4.2
author: sstent author: sstent
index: icalendar.ts index: icalendar.ts
# Legacy SilverBullet permission name # Legacy SilverBullet permission name

View File

@@ -65,16 +65,45 @@ async function sha256Hash(str: string): Promise<string> {
} }
/** /**
* Converts Date to local time string (browser's timezone) * Converts UTC Date to a specific timezone string
* Uses Intl.DateTimeFormat to properly handle timezone conversion
*/ */
export function localDateString(date: Date): string { export function dateToTimezoneString(date: Date, timezone: string = "America/Los_Angeles"): string {
const pad = (n: number) => String(n).padStart(2, "0"); try {
return date.getFullYear() + "-" + // Get date components in the target timezone
pad(date.getMonth() + 1) + "-" + const formatter = new Intl.DateTimeFormat('en-US', {
pad(date.getDate()) + "T" + timeZone: timezone,
pad(date.getHours()) + ":" + year: 'numeric',
pad(date.getMinutes()) + ":" + month: '2-digit',
pad(date.getSeconds()); day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const parts = formatter.formatToParts(date);
const values: Record<string, string> = {};
for (const part of parts) {
if (part.type !== 'literal') {
values[part.type] = part.value;
}
}
// Format as ISO-like string: YYYY-MM-DDTHH:MM:SS
return `${values.year}-${values.month}-${values.day}T${values.hour}:${values.minute}:${values.second}`;
} catch (err) {
console.error(`[iCalendar] Error converting to timezone ${timezone}:`, err);
// Fallback to UTC
const pad = (n: number) => String(n).padStart(2, "0");
return date.getUTCFullYear() + "-" +
pad(date.getUTCMonth() + 1) + "-" +
pad(date.getUTCDate()) + "T" +
pad(date.getUTCHours()) + ":" +
pad(date.getUTCMinutes()) + ":" +
pad(date.getUTCSeconds());
}
} }
/** /**
@@ -122,13 +151,15 @@ function convertDatesToStrings<T>(obj: T): any {
// Configuration Functions // Configuration Functions
// ============================================================================ // ============================================================================
async function getSources(): Promise<{ sources: any[], syncWindowDays: number }> { async function getSources(): Promise<{ sources: any[], syncWindowDays: number, displayTimezone: string }> {
try { try {
const rawConfig = await config.get("icalendar", { sources: [] }) as any; const rawConfig = await config.get("icalendar", { sources: [] }) as any;
console.log("[iCalendar] Raw config retrieved:", JSON.stringify(rawConfig)); console.log("[iCalendar] Raw config retrieved:", JSON.stringify(rawConfig));
let sources = rawConfig.sources || []; let sources = rawConfig.sources || [];
const syncWindowDays = rawConfig.syncWindowDays || 365; const syncWindowDays = rawConfig.syncWindowDays || 365;
// Get user's display timezone, default to America/Los_Angeles (PST)
const displayTimezone = rawConfig.displayTimezone || "America/Los_Angeles";
if (sources && typeof sources === "object" && !Array.isArray(sources)) { if (sources && typeof sources === "object" && !Array.isArray(sources)) {
const sourceArray = []; const sourceArray = [];
@@ -140,10 +171,10 @@ async function getSources(): Promise<{ sources: any[], syncWindowDays: number }>
sources = sourceArray; sources = sourceArray;
} }
return { sources, syncWindowDays }; return { sources, syncWindowDays, displayTimezone };
} catch (e) { } catch (e) {
console.error("[iCalendar] Error in getSources:", e); console.error("[iCalendar] Error in getSources:", e);
return { sources: [], syncWindowDays: 365 }; return { sources: [], syncWindowDays: 365, displayTimezone: "America/Los_Angeles" };
} }
} }
@@ -213,7 +244,7 @@ async function resolveEventEnd(icsEvent: any): Promise<Date | null> {
/** /**
* Expands recurring events into individual occurrences. * Expands recurring events into individual occurrences.
*/ */
export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Date()): any[] { export function expandRecurrences(icsEvent: any, windowDays = 365, displayTimezone = "America/Los_Angeles", now = new Date()): any[] {
const rruleStr = icsEvent.rrule || (icsEvent as any).recurrenceRule; const rruleStr = icsEvent.rrule || (icsEvent as any).recurrenceRule;
if (!rruleStr) return [icsEvent]; if (!rruleStr) return [icsEvent];
@@ -268,9 +299,9 @@ export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Dat
return { return {
...icsEvent, ...icsEvent,
start: occurrenceDate.toISOString(), start: occurrenceDate.toISOString(),
startLocal: localDateString(occurrenceDate), startLocal: dateToTimezoneString(occurrenceDate, displayTimezone),
end: endDate ? endDate.toISOString() : undefined, end: endDate ? endDate.toISOString() : undefined,
endLocal: endDate ? localDateString(endDate) : undefined, endLocal: endDate ? dateToTimezoneString(endDate, displayTimezone) : undefined,
recurrent: true, recurrent: true,
rrule: undefined, rrule: undefined,
}; };
@@ -283,7 +314,7 @@ export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Dat
} }
} }
async function fetchAndParseCalendar(source: any, windowDays = 365): Promise<any[]> { async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimezone = "America/Los_Angeles"): Promise<any[]> {
try { try {
const response = await fetch(source.url); const response = await fetch(source.url);
if (!response.ok) { if (!response.ok) {
@@ -314,9 +345,9 @@ async function fetchAndParseCalendar(source: any, windowDays = 365): Promise<any
name: icsEvent.summary || "Untitled Event", name: icsEvent.summary || "Untitled Event",
// Store both UTC (for sorting/comparison) and local (for display) // Store both UTC (for sorting/comparison) and local (for display)
start: startDateUTC.toISOString(), start: startDateUTC.toISOString(),
startLocal: localDateString(startDateUTC), startLocal: dateToTimezoneString(startDateUTC, displayTimezone),
end: endDateUTC ? endDateUTC.toISOString() : undefined, end: endDateUTC ? endDateUTC.toISOString() : undefined,
endLocal: endDateUTC ? localDateString(endDateUTC) : undefined, endLocal: endDateUTC ? dateToTimezoneString(endDateUTC, displayTimezone) : undefined,
tag: "ical-event", tag: "ical-event",
sourceName: source.name, sourceName: source.name,
timezone: rawTz timezone: rawTz
@@ -326,7 +357,7 @@ async function fetchAndParseCalendar(source: any, windowDays = 365): Promise<any
baseEvent.description = `(Warning: Unknown timezone "${rawTz}") ${baseEvent.description || ""}`; baseEvent.description = `(Warning: Unknown timezone "${rawTz}") ${baseEvent.description || ""}`;
} }
const expanded = expandRecurrences(baseEvent, windowDays); const expanded = expandRecurrences(baseEvent, windowDays, displayTimezone);
for (const occurrence of expanded) { for (const occurrence of expanded) {
// Use summary in key to avoid collisions // Use summary in key to avoid collisions
const uniqueKey = `${occurrence.start}${occurrence.uid || ''}${occurrence.summary || ''}`; const uniqueKey = `${occurrence.start}${occurrence.uid || ''}${occurrence.summary || ''}`;
@@ -343,13 +374,14 @@ async function fetchAndParseCalendar(source: any, windowDays = 365): Promise<any
export async function syncCalendars() { export async function syncCalendars() {
try { try {
const { sources, syncWindowDays } = await getSources(); const { sources, syncWindowDays, displayTimezone } = await getSources();
if (sources.length === 0) return; if (sources.length === 0) return;
console.log(`[iCalendar] Using display timezone: ${displayTimezone}`);
await editor.flashNotification("Syncing calendars...", "info"); await editor.flashNotification("Syncing calendars...", "info");
const allEvents: any[] = []; const allEvents: any[] = [];
for (const source of sources) { for (const source of sources) {
const events = await fetchAndParseCalendar(source, syncWindowDays); const events = await fetchAndParseCalendar(source, syncWindowDays, displayTimezone);
allEvents.push(...events); allEvents.push(...events);
} }
await index.indexObjects("$icalendar", allEvents); await index.indexObjects("$icalendar", allEvents);