@luckydye/calendar 1.1.2 → 1.2.0
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/package.json +5 -4
- package/src/CalDAVConfig.ts +25 -0
- package/src/CalendarIntegration.ts +1 -2
- package/src/CalendarInternal.ts +20 -1
- package/src/CalendarView.ts +747 -374
- package/src/GoogleCalendarSource.ts +0 -7
- package/src/InMemorySource.ts +0 -15
- package/src/InhouseBookingSource.ts +331 -46
- package/src/Keybinds.ts +46 -0
- package/src/app.css +28 -0
- package/src/app.ts +175 -63
- package/dist/calendar.js +0 -5800
package/src/app.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { InhouseBookingSource } from "./InhouseBookingSource.js";
|
|
|
15
15
|
import { queueStatus } from "./StatusMessage.js";
|
|
16
16
|
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
17
17
|
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
18
|
+
import { registerKeybinds } from "./Keybinds.js";
|
|
18
19
|
import "./StatusBar.js";
|
|
19
20
|
import { NotificationScheduler } from "./NotificationScheduler.js";
|
|
20
21
|
import serviceWorkerUrl from "./service-worker.js?url";
|
|
@@ -34,7 +35,19 @@ const calendarElement = document.querySelector(
|
|
|
34
35
|
) as CalendarViewElement;
|
|
35
36
|
const calendar = calendarElement.internal;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
registerKeybinds(
|
|
39
|
+
[
|
|
40
|
+
{ key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
|
|
41
|
+
{ key: "c", cmdOrCtrl: true, action: () => calendarElement.copySelectedEvents() },
|
|
42
|
+
{ key: "Backspace", action: () => calendarElement.deleteSelectedEvents() },
|
|
43
|
+
{ key: "Delete", action: () => calendarElement.deleteSelectedEvents() },
|
|
44
|
+
{ key: "Escape", action: () => calendarElement.escape() },
|
|
45
|
+
{ key: "t", action: () => calendarElement.scrollToToday() },
|
|
46
|
+
],
|
|
47
|
+
calendarElement,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let workerPromise: Promise<ServiceWorkerRegistration> | undefined;
|
|
38
51
|
|
|
39
52
|
// Register service worker for PWA
|
|
40
53
|
if ("serviceWorker" in navigator) {
|
|
@@ -107,8 +120,30 @@ async function sync(cal: Calendar, options?: { force?: boolean }): Promise<void>
|
|
|
107
120
|
(async () => {
|
|
108
121
|
await calendar.initPromise;
|
|
109
122
|
|
|
110
|
-
//
|
|
123
|
+
// Register sample source
|
|
124
|
+
const sampleSource = new InMemorySource('sample', "Sample Events", '#10b981');
|
|
125
|
+
const sampleCalendar = createInMemoryCalendar(sampleSource);
|
|
126
|
+
loadedCalendars.set(sampleCalendar.id, sampleCalendar);
|
|
127
|
+
activeCalendarStore.registerCalendar(sampleCalendar);
|
|
128
|
+
|
|
129
|
+
const button = document.createElement("button");
|
|
130
|
+
button.className = "toolbar-button";
|
|
131
|
+
button.title = "CalDAV Sources";
|
|
132
|
+
button.innerHTML = "📅";
|
|
133
|
+
button.addEventListener("click", () => {
|
|
134
|
+
showCalDAVConfig();
|
|
135
|
+
});
|
|
136
|
+
button.slot = "toolbar-center";
|
|
137
|
+
calendarElement.appendChild(button);
|
|
138
|
+
|
|
139
|
+
// Create the active calendar selector UI
|
|
140
|
+
createActiveCalendarSelector();
|
|
141
|
+
|
|
142
|
+
// Sync CalDAV sources (this will call updateEnabledCalendars after calendars are registered)
|
|
111
143
|
await syncCalDAV();
|
|
144
|
+
|
|
145
|
+
// Update active calendar color after sync
|
|
146
|
+
updateActiveCalendarColor();
|
|
112
147
|
})();
|
|
113
148
|
|
|
114
149
|
// CalDAV configuration UI
|
|
@@ -219,6 +254,7 @@ async function createCalDAVCalendars(
|
|
|
219
254
|
name: calInfo.displayName,
|
|
220
255
|
color: calInfo.color || source.color,
|
|
221
256
|
enabled: source.enabled,
|
|
257
|
+
locked: (source as { locked?: boolean }).locked,
|
|
222
258
|
sourceId: source.id,
|
|
223
259
|
sourceType: "caldav",
|
|
224
260
|
calendarUrl: calInfo.url,
|
|
@@ -240,14 +276,6 @@ async function createCalDAVCalendars(
|
|
|
240
276
|
return caldavSource.updateEvent(id, { ...updates, calendarId: calInfo.url });
|
|
241
277
|
},
|
|
242
278
|
|
|
243
|
-
async moveEvent(
|
|
244
|
-
id: string,
|
|
245
|
-
newStart: Date,
|
|
246
|
-
newEnd: Date,
|
|
247
|
-
): Promise<CalendarEvent> {
|
|
248
|
-
return caldavSource.updateEvent(id, { start: newStart, end: newEnd, calendarId: calInfo.url });
|
|
249
|
-
},
|
|
250
|
-
|
|
251
279
|
async deleteEvent(id: string): Promise<void> {
|
|
252
280
|
const eventUrl = `${calInfo.url.replace(/\/$/, '')}/${id}.ics`;
|
|
253
281
|
await caldavSource.request(eventUrl, 'DELETE', undefined, {});
|
|
@@ -265,6 +293,7 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
265
293
|
name: source.name,
|
|
266
294
|
color: source.color,
|
|
267
295
|
enabled: source.enabled,
|
|
296
|
+
locked: (source as { locked?: boolean }).locked,
|
|
268
297
|
sourceId: source.id,
|
|
269
298
|
sourceType: "ical",
|
|
270
299
|
|
|
@@ -272,7 +301,7 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
272
301
|
const result = await fetchICalEventsWithNotifications(source.credentials, source.color, source.name);
|
|
273
302
|
return result.events;
|
|
274
303
|
},
|
|
275
|
-
// iCal calendars are read-only
|
|
304
|
+
// iCal calendars are read-only by default
|
|
276
305
|
};
|
|
277
306
|
}
|
|
278
307
|
|
|
@@ -297,6 +326,7 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
297
326
|
name: source.name,
|
|
298
327
|
color: source.color,
|
|
299
328
|
enabled: source.enabled,
|
|
329
|
+
locked: (source as { locked?: boolean }).locked,
|
|
300
330
|
sourceId: source.id,
|
|
301
331
|
sourceType: "google",
|
|
302
332
|
calendarUrl: "https://calendar.google.com/calendar/u/0/r?tab=mc",
|
|
@@ -318,14 +348,6 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
318
348
|
return googleSource.updateEvent(id, updates);
|
|
319
349
|
},
|
|
320
350
|
|
|
321
|
-
async moveEvent(
|
|
322
|
-
id: string,
|
|
323
|
-
newStart: Date,
|
|
324
|
-
newEnd: Date,
|
|
325
|
-
): Promise<CalendarEvent> {
|
|
326
|
-
return googleSource.moveEvent(id, newStart, newEnd);
|
|
327
|
-
},
|
|
328
|
-
|
|
329
351
|
async deleteEvent(id: string): Promise<void> {
|
|
330
352
|
return googleSource.deleteEvent(id);
|
|
331
353
|
},
|
|
@@ -351,12 +373,34 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
351
373
|
name: source.name,
|
|
352
374
|
color: source.color,
|
|
353
375
|
enabled: source.enabled,
|
|
376
|
+
locked: (source as { locked?: boolean }).locked,
|
|
354
377
|
sourceId: source.id,
|
|
355
378
|
sourceType: "inhouse",
|
|
356
379
|
|
|
357
380
|
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
358
381
|
return inhouseSource.fetchEvents();
|
|
359
382
|
},
|
|
383
|
+
|
|
384
|
+
async createEvent(
|
|
385
|
+
event: Omit<CalendarEvent, "calendar" | "color">,
|
|
386
|
+
): Promise<CalendarEvent> {
|
|
387
|
+
return inhouseSource.createEvent(event);
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
async updateEvent(
|
|
391
|
+
id: string,
|
|
392
|
+
updates: Partial<CalendarEvent>,
|
|
393
|
+
): Promise<CalendarEvent> {
|
|
394
|
+
return inhouseSource.updateEvent(id, updates);
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async deleteEvent(id: string): Promise<void> {
|
|
398
|
+
return inhouseSource.deleteEvent(id);
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
async fetchProjects(): Promise<Array<{ id: number; name: string }>> {
|
|
402
|
+
return inhouseSource.fetchProjects();
|
|
403
|
+
},
|
|
360
404
|
};
|
|
361
405
|
}
|
|
362
406
|
|
|
@@ -391,16 +435,6 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
|
|
|
391
435
|
return source.updateEvent(id, updates);
|
|
392
436
|
},
|
|
393
437
|
|
|
394
|
-
async moveEvent(
|
|
395
|
-
id: string,
|
|
396
|
-
newStart: Date,
|
|
397
|
-
newEnd: Date,
|
|
398
|
-
): Promise<CalendarEvent> {
|
|
399
|
-
if (!source.moveEvent)
|
|
400
|
-
throw new Error("Source does not support moveEvent");
|
|
401
|
-
return source.moveEvent(id, newStart, newEnd);
|
|
402
|
-
},
|
|
403
|
-
|
|
404
438
|
async deleteEvent(id: string): Promise<void> {
|
|
405
439
|
if (!source.deleteEvent)
|
|
406
440
|
throw new Error("Source does not support deleteEvent");
|
|
@@ -443,6 +477,8 @@ async function syncCalDAV(force = false) {
|
|
|
443
477
|
configuredSourceIds = newSourceIds;
|
|
444
478
|
|
|
445
479
|
for (const source of sources) {
|
|
480
|
+
if (!source.enabled) continue;
|
|
481
|
+
|
|
446
482
|
if (source.type === "caldav") {
|
|
447
483
|
if (
|
|
448
484
|
!source.credentials?.serverUrl ||
|
|
@@ -508,7 +544,6 @@ async function syncCalDAV(force = false) {
|
|
|
508
544
|
continue;
|
|
509
545
|
}
|
|
510
546
|
|
|
511
|
-
// Inhouse Booking sources have one calendar
|
|
512
547
|
const calendar = createInhouseCalendar(source);
|
|
513
548
|
loadedCalendars.set(calendar.id, calendar);
|
|
514
549
|
activeCalendarStore.registerCalendar(calendar);
|
|
@@ -525,6 +560,7 @@ async function syncCalDAV(force = false) {
|
|
|
525
560
|
|
|
526
561
|
// Update enabled calendars filter
|
|
527
562
|
updateEnabledCalendars();
|
|
563
|
+
updateLockedCalendars();
|
|
528
564
|
|
|
529
565
|
// Update the UI selector and color after sync
|
|
530
566
|
updateActiveCalendarSelector();
|
|
@@ -545,6 +581,75 @@ calendarElement.addEventListener("selection-change", (e) => {
|
|
|
545
581
|
console.log("Selection changed:", e.detail.selectedEvents);
|
|
546
582
|
});
|
|
547
583
|
|
|
584
|
+
function showProjectPickerDialog(
|
|
585
|
+
projects: Array<{ id: number; name: string }>,
|
|
586
|
+
): Promise<number | null> {
|
|
587
|
+
return new Promise((resolve) => {
|
|
588
|
+
let resolved = false;
|
|
589
|
+
const finish = (value: number | null) => {
|
|
590
|
+
if (resolved) return;
|
|
591
|
+
resolved = true;
|
|
592
|
+
resolve(value);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const dialog = document.createElement("dialog");
|
|
596
|
+
dialog.style.cssText =
|
|
597
|
+
"background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--grid-color-strong);border-radius:var(--border-radius);padding:16px;min-width:300px;max-width:480px;";
|
|
598
|
+
|
|
599
|
+
const input = document.createElement("input");
|
|
600
|
+
input.type = "text";
|
|
601
|
+
input.placeholder = "Search projects...";
|
|
602
|
+
input.style.cssText =
|
|
603
|
+
"width:100%;padding:6px 8px;margin-bottom:8px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--grid-color-strong);border-radius:var(--border-radius-sm);box-sizing:border-box;";
|
|
604
|
+
|
|
605
|
+
const list = document.createElement("div");
|
|
606
|
+
list.style.cssText =
|
|
607
|
+
"max-height:320px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;";
|
|
608
|
+
|
|
609
|
+
const renderList = (filter: string) => {
|
|
610
|
+
list.innerHTML = "";
|
|
611
|
+
const filtered = filter
|
|
612
|
+
? projects.filter((p) =>
|
|
613
|
+
p.name.toLowerCase().includes(filter.toLowerCase()),
|
|
614
|
+
)
|
|
615
|
+
: projects;
|
|
616
|
+
for (const project of filtered) {
|
|
617
|
+
const item = document.createElement("button");
|
|
618
|
+
item.textContent = project.name;
|
|
619
|
+
item.style.cssText =
|
|
620
|
+
"text-align:left;padding:6px 8px;background:var(--bg-item);color:var(--text-primary);border:none;border-radius:var(--border-radius-sm);cursor:pointer;";
|
|
621
|
+
item.addEventListener("mouseenter", () => {
|
|
622
|
+
item.style.background = "var(--bg-item-hover)";
|
|
623
|
+
});
|
|
624
|
+
item.addEventListener("mouseleave", () => {
|
|
625
|
+
item.style.background = "var(--bg-item)";
|
|
626
|
+
});
|
|
627
|
+
item.addEventListener("click", () => {
|
|
628
|
+
finish(project.id);
|
|
629
|
+
dialog.close();
|
|
630
|
+
});
|
|
631
|
+
list.appendChild(item);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
renderList("");
|
|
636
|
+
input.addEventListener("input", () => renderList(input.value));
|
|
637
|
+
|
|
638
|
+
dialog.addEventListener("close", () => {
|
|
639
|
+
dialog.remove();
|
|
640
|
+
finish(null);
|
|
641
|
+
});
|
|
642
|
+
dialog.addEventListener("click", (ev) => {
|
|
643
|
+
if (ev.target === dialog) dialog.close();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
dialog.append(input, list);
|
|
647
|
+
document.body.appendChild(dialog);
|
|
648
|
+
dialog.showModal();
|
|
649
|
+
input.focus();
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
548
653
|
calendarElement.addEventListener("create-event", async (e) => {
|
|
549
654
|
const { start, end } = e.detail;
|
|
550
655
|
const calendar = getActiveCalendar();
|
|
@@ -558,14 +663,19 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
558
663
|
return;
|
|
559
664
|
}
|
|
560
665
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
666
|
+
let id = `event-${Date.now()}`;
|
|
667
|
+
|
|
668
|
+
if (calendar.sourceType === "inhouse") {
|
|
669
|
+
const inhouseCalendar = calendar as typeof calendar & {
|
|
670
|
+
fetchProjects(): Promise<Array<{ id: number; name: string }>>;
|
|
671
|
+
};
|
|
672
|
+
const projects = await inhouseCalendar.fetchProjects();
|
|
673
|
+
const projectId = await showProjectPickerDialog(projects);
|
|
674
|
+
if (projectId === null) return; // user cancelled
|
|
675
|
+
id = `0:0:${projectId}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
await calendar.createEvent({ id, title: "New Event", start, end });
|
|
569
679
|
await sync(calendar, { force: true });
|
|
570
680
|
updateStatusBar();
|
|
571
681
|
});
|
|
@@ -580,7 +690,7 @@ calendarElement.addEventListener("move-event", async (e) => {
|
|
|
580
690
|
return;
|
|
581
691
|
}
|
|
582
692
|
|
|
583
|
-
if (!calendar.
|
|
693
|
+
if (!calendar.updateEvent) {
|
|
584
694
|
queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
|
|
585
695
|
return;
|
|
586
696
|
}
|
|
@@ -596,7 +706,7 @@ calendarElement.addEventListener("move-event", async (e) => {
|
|
|
596
706
|
calendar.name,
|
|
597
707
|
);
|
|
598
708
|
try {
|
|
599
|
-
await calendar.
|
|
709
|
+
await calendar.updateEvent(event.id, { start, end });
|
|
600
710
|
await sync(calendar, { force: true });
|
|
601
711
|
console.log("Move completed on calendar:", calendar.name);
|
|
602
712
|
updateStatusBar();
|
|
@@ -688,10 +798,15 @@ calendarElement.addEventListener("delete-events", async (e) => {
|
|
|
688
798
|
":",
|
|
689
799
|
calendarEvents.map((ev) => ev.id),
|
|
690
800
|
);
|
|
691
|
-
|
|
692
|
-
|
|
801
|
+
try {
|
|
802
|
+
for (const event of calendarEvents) {
|
|
803
|
+
await calendar.deleteEvent(event.id);
|
|
804
|
+
}
|
|
805
|
+
await sync(calendar, { force: true });
|
|
806
|
+
} catch (error) {
|
|
807
|
+
console.error("Failed to delete event:", error);
|
|
808
|
+
queueStatus(`Failed to delete event: ${error instanceof Error ? error.message : String(error)}`);
|
|
693
809
|
}
|
|
694
|
-
await sync(calendar, { force: true });
|
|
695
810
|
}
|
|
696
811
|
console.log("Delete completed");
|
|
697
812
|
updateStatusBar();
|
|
@@ -781,25 +896,6 @@ calendarElement.addEventListener("import-ical", async (e) => {
|
|
|
781
896
|
}
|
|
782
897
|
});
|
|
783
898
|
|
|
784
|
-
// --- In-Memory Source for Testing ---
|
|
785
|
-
let sampleSource: InMemorySource | null = null;
|
|
786
|
-
|
|
787
|
-
(async () => {
|
|
788
|
-
sampleSource = new InMemorySource('sample', "Sample Events", '#10b981');
|
|
789
|
-
|
|
790
|
-
// Register the sample source as a calendar
|
|
791
|
-
const sampleCalendar = createInMemoryCalendar(sampleSource);
|
|
792
|
-
loadedCalendars.set(sampleCalendar.id, sampleCalendar);
|
|
793
|
-
activeCalendarStore.registerCalendar(sampleCalendar);
|
|
794
|
-
|
|
795
|
-
// Create the active calendar selector UI
|
|
796
|
-
createActiveCalendarSelector();
|
|
797
|
-
|
|
798
|
-
// Update enabled calendars and active calendar color
|
|
799
|
-
updateEnabledCalendars();
|
|
800
|
-
updateActiveCalendarColor();
|
|
801
|
-
})();
|
|
802
|
-
|
|
803
899
|
// --- Active Calendar Selector UI ---
|
|
804
900
|
function createActiveCalendarSelector() {
|
|
805
901
|
// Check if selector already exists in the slot
|
|
@@ -819,7 +915,6 @@ function createActiveCalendarSelector() {
|
|
|
819
915
|
`;
|
|
820
916
|
|
|
821
917
|
const label = document.createElement("span");
|
|
822
|
-
label.textContent = "Active:";
|
|
823
918
|
label.style.cssText = `
|
|
824
919
|
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
825
920
|
white-space: nowrap;
|
|
@@ -889,6 +984,23 @@ function updateEnabledCalendars() {
|
|
|
889
984
|
calendar.setEnabledCalendars(enabledCalendarIdentifiers);
|
|
890
985
|
}
|
|
891
986
|
|
|
987
|
+
function updateLockedCalendars() {
|
|
988
|
+
const lockedCalendarIdentifiers = Array.from(loadedCalendars.values())
|
|
989
|
+
.filter(cal => (cal as { locked?: boolean }).locked)
|
|
990
|
+
.flatMap(cal => {
|
|
991
|
+
// For CalDAV, use the calendar URL; for others, use the source ID
|
|
992
|
+
const identifiers = [];
|
|
993
|
+
if (cal.calendarUrl) {
|
|
994
|
+
identifiers.push(cal.calendarUrl);
|
|
995
|
+
}
|
|
996
|
+
if (cal.sourceId) {
|
|
997
|
+
identifiers.push(cal.sourceId);
|
|
998
|
+
}
|
|
999
|
+
return identifiers;
|
|
1000
|
+
});
|
|
1001
|
+
calendar.setLockedCalendars(lockedCalendarIdentifiers);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
892
1004
|
function getActiveCalendarSelect(): HTMLSelectElement | null {
|
|
893
1005
|
// Look in the light DOM first (where slotted content lives)
|
|
894
1006
|
const selector = document.getElementById("active-calendar-selector");
|