@luckydye/calendar 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar.js +2198 -1539
- package/package.json +2 -2
- package/src/CalDAVConfig.ts +50 -1
- package/src/CalDAVSource.ts +52 -19
- package/src/CalendarIntegration.ts +2 -2
- package/src/CalendarInternal.ts +10 -4
- package/src/CalendarLayer.ts +28 -0
- package/src/CalendarView.ts +517 -1146
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +27 -1
- package/src/ICal.ts +7 -2
- package/src/InMemorySource.ts +6 -6
- package/src/IndexedDBStorage.ts +6 -0
- package/src/InhouseBookingSource.ts +2 -1
- package/src/Keybinds.ts +3 -18
- package/src/StatusBar.ts +11 -0
- package/src/Theme.ts +4 -4
- package/src/TimeseriesJson.ts +114 -0
- package/src/app.ts +199 -48
- package/src/layers/EventsLayer.ts +958 -0
- package/src/layers/GridLayer.ts +296 -0
- package/src/layers/TimeseriesHeatmapLayer.ts +132 -0
- package/src/lib.ts +1 -0
package/src/app.ts
CHANGED
|
@@ -12,9 +12,11 @@ import { initializeTheme } from "./Theme.js";
|
|
|
12
12
|
import { InMemorySource } from "./InMemorySource.js";
|
|
13
13
|
import { GoogleCalendarSource } from "./GoogleCalendarSource.js";
|
|
14
14
|
import { InhouseBookingSource } from "./InhouseBookingSource.js";
|
|
15
|
+
import { fetchTimeseriesJsonEvents } from "./TimeseriesJson.js";
|
|
15
16
|
import { queueStatus } from "./StatusMessage.js";
|
|
16
17
|
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
17
18
|
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
19
|
+
import { sanitizeEventDescription } from "./DescriptionSanitizer.js";
|
|
18
20
|
import { registerKeybinds } from "./Keybinds.js";
|
|
19
21
|
import "./StatusBar.js";
|
|
20
22
|
import { NotificationScheduler } from "./NotificationScheduler.js";
|
|
@@ -35,6 +37,136 @@ const calendarElement = document.querySelector(
|
|
|
35
37
|
) as CalendarViewElement;
|
|
36
38
|
const calendar = calendarElement.internal;
|
|
37
39
|
|
|
40
|
+
type PromptApiLanguageModel = {
|
|
41
|
+
availability?: () => Promise<string>;
|
|
42
|
+
create: (options?: unknown) => Promise<{
|
|
43
|
+
promptStreaming: (
|
|
44
|
+
prompt: string,
|
|
45
|
+
options?: unknown,
|
|
46
|
+
) => Promise<ReadableStream<string>> | ReadableStream<string>;
|
|
47
|
+
destroy?: () => void;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getPromptApiLanguageModel(): PromptApiLanguageModel | null {
|
|
52
|
+
const fromGlobal = (globalThis as { LanguageModel?: unknown }).LanguageModel;
|
|
53
|
+
if (fromGlobal) return fromGlobal as PromptApiLanguageModel;
|
|
54
|
+
|
|
55
|
+
const fromWindow = (
|
|
56
|
+
window as { ai?: { languageModel?: unknown } }
|
|
57
|
+
).ai?.languageModel;
|
|
58
|
+
if (!fromWindow) return null;
|
|
59
|
+
return fromWindow as PromptApiLanguageModel;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let summaryAbortController: AbortController | null = null;
|
|
63
|
+
let summarySession: { destroy?: () => void } | null = null;
|
|
64
|
+
let summaryRequestToken = 0;
|
|
65
|
+
let activeSummaryKey: string | null = null;
|
|
66
|
+
|
|
67
|
+
function abortActiveDescriptionSummary(): void {
|
|
68
|
+
if (summaryAbortController) {
|
|
69
|
+
summaryAbortController.abort();
|
|
70
|
+
summaryAbortController = null;
|
|
71
|
+
}
|
|
72
|
+
if (summarySession) {
|
|
73
|
+
summarySession.destroy?.();
|
|
74
|
+
summarySession = null;
|
|
75
|
+
}
|
|
76
|
+
if (activeSummaryKey) {
|
|
77
|
+
calendarElement.cancelDescriptionSummary(activeSummaryKey);
|
|
78
|
+
activeSummaryKey = null;
|
|
79
|
+
}
|
|
80
|
+
summaryRequestToken++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function streamDescriptionSummary(key: string, description: string): Promise<void> {
|
|
84
|
+
const sanitizedDescription = sanitizeEventDescription(description);
|
|
85
|
+
abortActiveDescriptionSummary();
|
|
86
|
+
activeSummaryKey = key;
|
|
87
|
+
const token = summaryRequestToken;
|
|
88
|
+
calendarElement.startDescriptionSummary(key);
|
|
89
|
+
|
|
90
|
+
const languageModel = getPromptApiLanguageModel();
|
|
91
|
+
if (!languageModel) {
|
|
92
|
+
calendarElement.failDescriptionSummary(key, "Prompt API not available in this browser.");
|
|
93
|
+
activeSummaryKey = null;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
summaryAbortController = controller;
|
|
99
|
+
let session: { destroy?: () => void } | null = null;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const availability = await languageModel.availability?.();
|
|
103
|
+
if (token !== summaryRequestToken || key !== activeSummaryKey) return;
|
|
104
|
+
if (availability === "unavailable") {
|
|
105
|
+
throw new Error("Prompt API model is unavailable.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
session = await languageModel.create();
|
|
109
|
+
summarySession = session;
|
|
110
|
+
const prompt = [
|
|
111
|
+
"Summarize this calendar event description in 2-3 concise sentences.",
|
|
112
|
+
"Keep critical details like dates, times, links, and action items.",
|
|
113
|
+
"Return plain text only.",
|
|
114
|
+
"",
|
|
115
|
+
sanitizedDescription,
|
|
116
|
+
].join("\n");
|
|
117
|
+
|
|
118
|
+
const stream = await Promise.resolve(
|
|
119
|
+
session.promptStreaming(prompt, { signal: controller.signal }),
|
|
120
|
+
);
|
|
121
|
+
const reader = stream.getReader();
|
|
122
|
+
try {
|
|
123
|
+
while (true) {
|
|
124
|
+
const { value, done } = await reader.read();
|
|
125
|
+
if (done) break;
|
|
126
|
+
if (
|
|
127
|
+
token !== summaryRequestToken ||
|
|
128
|
+
key !== activeSummaryKey ||
|
|
129
|
+
controller.signal.aborted
|
|
130
|
+
) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (value) {
|
|
134
|
+
calendarElement.appendDescriptionSummaryChunk(key, value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
reader.releaseLock();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (token !== summaryRequestToken || key !== activeSummaryKey) return;
|
|
142
|
+
calendarElement.finishDescriptionSummary(key);
|
|
143
|
+
activeSummaryKey = null;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (
|
|
146
|
+
controller.signal.aborted ||
|
|
147
|
+
token !== summaryRequestToken ||
|
|
148
|
+
key !== activeSummaryKey
|
|
149
|
+
) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
calendarElement.failDescriptionSummary(
|
|
153
|
+
key,
|
|
154
|
+
error instanceof Error
|
|
155
|
+
? error.message
|
|
156
|
+
: "Failed to generate description summary.",
|
|
157
|
+
);
|
|
158
|
+
activeSummaryKey = null;
|
|
159
|
+
} finally {
|
|
160
|
+
if (summaryAbortController === controller) {
|
|
161
|
+
summaryAbortController = null;
|
|
162
|
+
}
|
|
163
|
+
if (summarySession === session) {
|
|
164
|
+
summarySession = null;
|
|
165
|
+
}
|
|
166
|
+
session?.destroy?.();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
38
170
|
registerKeybinds(
|
|
39
171
|
[
|
|
40
172
|
{ key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
|
|
@@ -44,7 +176,6 @@ registerKeybinds(
|
|
|
44
176
|
{ key: "Escape", action: () => calendarElement.escape() },
|
|
45
177
|
{ key: "t", action: () => calendarElement.scrollToToday() },
|
|
46
178
|
],
|
|
47
|
-
calendarElement,
|
|
48
179
|
);
|
|
49
180
|
|
|
50
181
|
let workerPromise: Promise<ServiceWorkerRegistration> | undefined;
|
|
@@ -80,6 +211,7 @@ function updateStatusBar() {
|
|
|
80
211
|
|
|
81
212
|
// Update statusbar on relevant events
|
|
82
213
|
calendarElement.addEventListener("selection-change", updateStatusBar);
|
|
214
|
+
calendarElement.addEventListener("meta-key-change", updateStatusBar);
|
|
83
215
|
|
|
84
216
|
// Update statusbar periodically for current time
|
|
85
217
|
setInterval(updateStatusBar, 10000); // Every 10 seconds
|
|
@@ -108,6 +240,21 @@ calendarElement.addEventListener("event-click", () => {
|
|
|
108
240
|
requestNotificationPermission();
|
|
109
241
|
}, { once: true });
|
|
110
242
|
|
|
243
|
+
calendarElement.addEventListener("request-description-summary", (e: Event) => {
|
|
244
|
+
const { key, description } = (e as CustomEvent).detail as {
|
|
245
|
+
key: string;
|
|
246
|
+
description: string;
|
|
247
|
+
};
|
|
248
|
+
void streamDescriptionSummary(key, description);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
calendarElement.addEventListener("cancel-description-summary", (e: Event) => {
|
|
252
|
+
const { key } = (e as CustomEvent).detail as { key: string };
|
|
253
|
+
if (key === activeSummaryKey) {
|
|
254
|
+
abortActiveDescriptionSummary();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
111
258
|
// Load iCal files
|
|
112
259
|
const integration = new CalendarIntegration(calendar);
|
|
113
260
|
|
|
@@ -276,10 +423,10 @@ async function createCalDAVCalendars(
|
|
|
276
423
|
},
|
|
277
424
|
|
|
278
425
|
async updateEvent(
|
|
279
|
-
|
|
426
|
+
event: CalendarEvent,
|
|
280
427
|
updates: Partial<CalendarEvent>,
|
|
281
428
|
): Promise<CalendarEvent> {
|
|
282
|
-
return caldavSource.updateEvent(
|
|
429
|
+
return caldavSource.updateEvent(event, { ...updates, calendarId: calInfo.url });
|
|
283
430
|
},
|
|
284
431
|
|
|
285
432
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -311,6 +458,29 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
311
458
|
};
|
|
312
459
|
}
|
|
313
460
|
|
|
461
|
+
// Create a Timeseries JSON calendar wrapper
|
|
462
|
+
function createTimeseriesJsonCalendar(source: CalendarSource): Calendar {
|
|
463
|
+
return {
|
|
464
|
+
id: source.id,
|
|
465
|
+
name: source.name,
|
|
466
|
+
color: source.color,
|
|
467
|
+
enabled: source.enabled,
|
|
468
|
+
locked: (source as { locked?: boolean }).locked,
|
|
469
|
+
sourceId: source.id,
|
|
470
|
+
sourceType: "timeseries-json",
|
|
471
|
+
|
|
472
|
+
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
473
|
+
return fetchTimeseriesJsonEvents(
|
|
474
|
+
source.credentials as { url: string; timestampField?: string; titleField?: string },
|
|
475
|
+
source.color,
|
|
476
|
+
source.name,
|
|
477
|
+
source.id,
|
|
478
|
+
);
|
|
479
|
+
},
|
|
480
|
+
// Timeseries JSON calendars are read-only by default
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
314
484
|
// Create a Google Calendar wrapper
|
|
315
485
|
function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
316
486
|
const calendarId = source.credentials.calendarId || "primary";
|
|
@@ -348,10 +518,10 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
348
518
|
},
|
|
349
519
|
|
|
350
520
|
async updateEvent(
|
|
351
|
-
|
|
521
|
+
event: CalendarEvent,
|
|
352
522
|
updates: Partial<CalendarEvent>,
|
|
353
523
|
): Promise<CalendarEvent> {
|
|
354
|
-
return googleSource.updateEvent(
|
|
524
|
+
return googleSource.updateEvent(event, updates);
|
|
355
525
|
},
|
|
356
526
|
|
|
357
527
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -395,10 +565,10 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
395
565
|
},
|
|
396
566
|
|
|
397
567
|
async updateEvent(
|
|
398
|
-
|
|
568
|
+
event: CalendarEvent,
|
|
399
569
|
updates: Partial<CalendarEvent>,
|
|
400
570
|
): Promise<CalendarEvent> {
|
|
401
|
-
return inhouseSource.updateEvent(
|
|
571
|
+
return inhouseSource.updateEvent(event, updates);
|
|
402
572
|
},
|
|
403
573
|
|
|
404
574
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -434,12 +604,12 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
|
|
|
434
604
|
},
|
|
435
605
|
|
|
436
606
|
async updateEvent(
|
|
437
|
-
|
|
607
|
+
event: CalendarEvent,
|
|
438
608
|
updates: Partial<CalendarEvent>,
|
|
439
609
|
): Promise<CalendarEvent> {
|
|
440
610
|
if (!source.updateEvent)
|
|
441
611
|
throw new Error("Source does not support updateEvent");
|
|
442
|
-
return source.updateEvent(
|
|
612
|
+
return source.updateEvent(event, updates);
|
|
443
613
|
},
|
|
444
614
|
|
|
445
615
|
async deleteEvent(id: string): Promise<void> {
|
|
@@ -538,6 +708,21 @@ async function syncCalDAV(force = false) {
|
|
|
538
708
|
} catch (error) {
|
|
539
709
|
console.error(`Sync error for iCal source ${source.name}:`, error);
|
|
540
710
|
}
|
|
711
|
+
} else if (source.type === "timeseries-json") {
|
|
712
|
+
if (!source.credentials?.url) {
|
|
713
|
+
console.warn(`Skipping Timeseries JSON source ${source.name}: missing URL`);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const calendar = createTimeseriesJsonCalendar(source);
|
|
718
|
+
loadedCalendars.set(calendar.id, calendar);
|
|
719
|
+
activeCalendarStore.registerCalendar(calendar);
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
await sync(calendar, { force });
|
|
723
|
+
} catch (error) {
|
|
724
|
+
console.error(`Sync error for Timeseries JSON source ${source.name}:`, error);
|
|
725
|
+
}
|
|
541
726
|
} else if (source.type === "google") {
|
|
542
727
|
if (!source.credentials?.accessToken) {
|
|
543
728
|
console.warn(`Skipping Google Calendar source ${source.name}: missing access token`);
|
|
@@ -722,7 +907,7 @@ function showProjectPickerDialog(
|
|
|
722
907
|
}
|
|
723
908
|
|
|
724
909
|
calendarElement.addEventListener("create-event", async (e) => {
|
|
725
|
-
const { start, end } = e.detail;
|
|
910
|
+
const { start, end, isAllDay } = e.detail;
|
|
726
911
|
const calendar = getActiveCalendar();
|
|
727
912
|
if (!calendar) {
|
|
728
913
|
queueStatus("No calendar selected. Please configure a calendar first.");
|
|
@@ -751,6 +936,7 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
751
936
|
title: "New Event",
|
|
752
937
|
start,
|
|
753
938
|
end,
|
|
939
|
+
isAllDay,
|
|
754
940
|
calendar: calendar.name,
|
|
755
941
|
calendarId: calendar.calendarUrl ?? calendar.id,
|
|
756
942
|
sourceId: calendar.sourceId,
|
|
@@ -758,7 +944,7 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
758
944
|
lastSynced: new Date(),
|
|
759
945
|
});
|
|
760
946
|
try {
|
|
761
|
-
await withMutation(calendar, () => calendar.createEvent({ id, title: "New Event", start, end }));
|
|
947
|
+
await withMutation(calendar, () => calendar.createEvent({ id, title: "New Event", start, end, isAllDay }));
|
|
762
948
|
} catch (error) {
|
|
763
949
|
calendarElement.internal.removeEventOptimistically(id);
|
|
764
950
|
console.error("Failed to create event:", error);
|
|
@@ -766,41 +952,6 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
766
952
|
}
|
|
767
953
|
});
|
|
768
954
|
|
|
769
|
-
calendarElement.addEventListener("move-event", async (e) => {
|
|
770
|
-
const { event, start, end } = e.detail;
|
|
771
|
-
|
|
772
|
-
const calendar = findCalendarForEvent(event);
|
|
773
|
-
|
|
774
|
-
if (!calendar) {
|
|
775
|
-
queueStatus("Cannot move event: calendar not found.");
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (!calendar.updateEvent) {
|
|
780
|
-
queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
console.log(
|
|
785
|
-
"Moving event:",
|
|
786
|
-
event.id,
|
|
787
|
-
"from",
|
|
788
|
-
event.start,
|
|
789
|
-
"to",
|
|
790
|
-
start,
|
|
791
|
-
"on calendar:",
|
|
792
|
-
calendar.name,
|
|
793
|
-
);
|
|
794
|
-
try {
|
|
795
|
-
calendarElement.internal.applyEventOptimistically({ ...event, start, end });
|
|
796
|
-
await withMutation(calendar, () => calendar.updateEvent(event.id, { start, end }));
|
|
797
|
-
} catch (error) {
|
|
798
|
-
calendarElement.internal.applyEventOptimistically(event);
|
|
799
|
-
console.error("Failed to move event:", error);
|
|
800
|
-
queueStatus(`Failed to move event: ${error instanceof Error ? error.message : String(error)}`);
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
|
|
804
955
|
calendarElement.addEventListener("update-event", async (e) => {
|
|
805
956
|
const { event, updates } = e.detail;
|
|
806
957
|
|
|
@@ -824,7 +975,7 @@ calendarElement.addEventListener("update-event", async (e) => {
|
|
|
824
975
|
);
|
|
825
976
|
try {
|
|
826
977
|
calendarElement.internal.applyEventOptimistically({ ...event, ...updates });
|
|
827
|
-
await withMutation(calendar, () => calendar.updateEvent(event
|
|
978
|
+
await withMutation(calendar, () => calendar.updateEvent(event, updates));
|
|
828
979
|
|
|
829
980
|
if ("reminders" in updates) {
|
|
830
981
|
const eventWithReminders = { ...event, ...updates };
|
|
@@ -992,6 +1143,7 @@ calendarElement.addEventListener("import-ical", async (e) => {
|
|
|
992
1143
|
function updateActiveCalendarColor() {
|
|
993
1144
|
const calendar = activeCalendarStore.getActiveCalendar();
|
|
994
1145
|
calendarElement.activeCalendarColor = calendar?.color ?? null;
|
|
1146
|
+
calendarElement.activeCalendarId = activeCalendarStore.getActiveId();
|
|
995
1147
|
}
|
|
996
1148
|
|
|
997
1149
|
function updateEnabledCalendars() {
|
|
@@ -1027,4 +1179,3 @@ function updateLockedCalendars() {
|
|
|
1027
1179
|
});
|
|
1028
1180
|
calendar.setLockedCalendars(lockedCalendarIdentifiers);
|
|
1029
1181
|
}
|
|
1030
|
-
|