From a096b8d18f49e60554f8cff88190546ff65ae6fa Mon Sep 17 00:00:00 2001 From: sstent Date: Sat, 21 Feb 2026 15:31:22 -0800 Subject: [PATCH] switch to ical.js - v4.2 --- PLUG.md | 2 +- deno.json | 2 +- icalendar.plug.yaml | 2 +- icalendar.ts | 74 ++++++++++++++++++++++++++++++++------------- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/PLUG.md b/PLUG.md index 54eaf0b..eceece8 100644 --- a/PLUG.md +++ b/PLUG.md @@ -1,6 +1,6 @@ --- name: Library/sstent/icalendar -version: "0.4.1" +version: "0.4.2" tags: meta/library files: - icalendar.plug.js diff --git a/deno.json b/deno.json index f71b6a3..b8c7f6f 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "icalendar-plug", - "version": "0.4.1", + "version": "0.4.2", "nodeModulesDir": "auto", "tasks": { "sync-version": "deno run -A scripts/sync-version.ts", diff --git a/icalendar.plug.yaml b/icalendar.plug.yaml index 50c3a25..90b511c 100644 --- a/icalendar.plug.yaml +++ b/icalendar.plug.yaml @@ -1,5 +1,5 @@ name: icalendar -version: 0.4.1 +version: 0.4.2 author: sstent index: icalendar.ts # Legacy SilverBullet permission name diff --git a/icalendar.ts b/icalendar.ts index 35323b1..498bf9a 100644 --- a/icalendar.ts +++ b/icalendar.ts @@ -65,16 +65,45 @@ async function sha256Hash(str: string): Promise { } /** - * 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 { - const pad = (n: number) => String(n).padStart(2, "0"); - return date.getFullYear() + "-" + - pad(date.getMonth() + 1) + "-" + - pad(date.getDate()) + "T" + - pad(date.getHours()) + ":" + - pad(date.getMinutes()) + ":" + - pad(date.getSeconds()); +export function dateToTimezoneString(date: Date, timezone: string = "America/Los_Angeles"): string { + try { + // Get date components in the target timezone + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + + const parts = formatter.formatToParts(date); + const values: Record = {}; + + 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(obj: T): any { // Configuration Functions // ============================================================================ -async function getSources(): Promise<{ sources: any[], syncWindowDays: number }> { +async function getSources(): Promise<{ sources: any[], syncWindowDays: number, displayTimezone: string }> { try { const rawConfig = await config.get("icalendar", { sources: [] }) as any; console.log("[iCalendar] Raw config retrieved:", JSON.stringify(rawConfig)); let sources = rawConfig.sources || []; 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)) { const sourceArray = []; @@ -140,10 +171,10 @@ async function getSources(): Promise<{ sources: any[], syncWindowDays: number }> sources = sourceArray; } - return { sources, syncWindowDays }; + return { sources, syncWindowDays, displayTimezone }; } catch (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 { /** * 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; if (!rruleStr) return [icsEvent]; @@ -268,9 +299,9 @@ export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Dat return { ...icsEvent, start: occurrenceDate.toISOString(), - startLocal: localDateString(occurrenceDate), + startLocal: dateToTimezoneString(occurrenceDate, displayTimezone), end: endDate ? endDate.toISOString() : undefined, - endLocal: endDate ? localDateString(endDate) : undefined, + endLocal: endDate ? dateToTimezoneString(endDate, displayTimezone) : undefined, recurrent: true, rrule: undefined, }; @@ -283,7 +314,7 @@ export function expandRecurrences(icsEvent: any, windowDays = 365, now = new Dat } } -async function fetchAndParseCalendar(source: any, windowDays = 365): Promise { +async function fetchAndParseCalendar(source: any, windowDays = 365, displayTimezone = "America/Los_Angeles"): Promise { try { const response = await fetch(source.url); if (!response.ok) { @@ -314,9 +345,9 @@ async function fetchAndParseCalendar(source: any, windowDays = 365): Promise