@luckydye/calendar 1.3.1 → 1.4.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/dist/calendar.js +2777 -2010
- package/package.json +7 -1
- package/src/ActiveCalendarStore.ts +88 -88
- package/src/CalDAVConfig.ts +611 -514
- package/src/CalDAVSource.ts +561 -433
- package/src/CalendarIntegration.ts +64 -47
- package/src/CalendarInternal.ts +645 -613
- package/src/CalendarLayer.ts +1 -0
- package/src/CalendarStorage.ts +51 -48
- package/src/CalendarView.ts +1085 -505
- package/src/Color.ts +48 -54
- package/src/DescriptionSanitizer.ts +10 -0
- package/src/GoogleCalendarSource.ts +758 -661
- package/src/ICal.ts +420 -343
- package/src/InMemorySource.ts +56 -48
- package/src/IndexedDBStorage.ts +444 -395
- package/src/InhouseBookingSource.ts +614 -522
- package/src/Keybinds.ts +6 -1
- package/src/NotificationScheduler.ts +11 -8
- package/src/StatusBar.ts +12 -8
- package/src/StatusMessage.ts +2 -2
- package/src/Theme.ts +21 -7
- package/src/TimeseriesJson.ts +98 -98
- package/src/app.ts +301 -115
- package/src/layers/EventsLayer.ts +530 -400
- package/src/layers/GridLayer.ts +45 -125
- package/src/layers/TimeseriesHeatmapLayer.ts +123 -120
- package/src/service-worker.js +3 -2
package/src/IndexedDBStorage.ts
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Attendee,
|
|
3
|
+
CalendarEvent,
|
|
4
|
+
NotificationConfig,
|
|
5
|
+
Organizer,
|
|
6
|
+
} from "./CalendarInternal.js";
|
|
2
7
|
import type { CalendarStorage, SyncMetadata } from "./CalendarStorage.js";
|
|
3
8
|
|
|
4
9
|
interface SerializedEvent {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
start: string; // ISO date string
|
|
13
|
+
end: string; // ISO date string
|
|
14
|
+
color?: string;
|
|
15
|
+
calendar?: string;
|
|
16
|
+
calendarId?: string;
|
|
17
|
+
sourceId?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
location?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
rrule?: string;
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
lastSynced?: string; // ISO date string
|
|
24
|
+
organizer?: Organizer;
|
|
25
|
+
attendees?: Attendee[];
|
|
26
|
+
status?: "TENTATIVE" | "CONFIRMED" | "CANCELLED";
|
|
27
|
+
isAllDay?: boolean;
|
|
28
|
+
reminders?: NotificationConfig[];
|
|
29
|
+
visualStyle?: "heatmap";
|
|
30
|
+
resourceUrl?: string;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
interface SerializedMetadata {
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
sourceId: string;
|
|
35
|
+
lastSync: string; // ISO date string
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -34,376 +40,419 @@ interface SerializedMetadata {
|
|
|
34
40
|
* Uses differential sync to efficiently update events.
|
|
35
41
|
*/
|
|
36
42
|
export class IndexedDBStorage implements CalendarStorage {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
43
|
+
dbName = "calendar-events";
|
|
44
|
+
storeName = "events";
|
|
45
|
+
metadataStoreName = "metadata";
|
|
46
|
+
version = 8;
|
|
47
|
+
|
|
48
|
+
db?: IDBDatabase;
|
|
49
|
+
openPromise?: Promise<IDBDatabase>;
|
|
50
|
+
|
|
51
|
+
async recreateDatabase(): Promise<void> {
|
|
52
|
+
if (this.db) {
|
|
53
|
+
this.db.close();
|
|
54
|
+
this.db = undefined;
|
|
55
|
+
}
|
|
56
|
+
this.openPromise = undefined;
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const deleteRequest = indexedDB.deleteDatabase(this.dbName);
|
|
60
|
+
|
|
61
|
+
deleteRequest.onerror = () => {
|
|
62
|
+
reject(
|
|
63
|
+
new Error(
|
|
64
|
+
`Failed to delete database: ${deleteRequest.error?.message}`,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
deleteRequest.onsuccess = () => {
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async open(): Promise<IDBDatabase> {
|
|
76
|
+
if (this.db) {
|
|
77
|
+
return this.db;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.openPromise) {
|
|
81
|
+
return this.openPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.openPromise = new Promise((resolve, reject) => {
|
|
85
|
+
const request = indexedDB.open(this.dbName, this.version);
|
|
86
|
+
|
|
87
|
+
request.onerror = () => {
|
|
88
|
+
this.openPromise = undefined;
|
|
89
|
+
reject(
|
|
90
|
+
new Error(`Failed to open IndexedDB: ${request.error?.message}`),
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
request.onsuccess = () => {
|
|
95
|
+
this.db = request.result;
|
|
96
|
+
this.openPromise = undefined;
|
|
97
|
+
resolve(request.result);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
request.onupgradeneeded = (event) => {
|
|
101
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
102
|
+
const transaction = (event.target as IDBOpenDBRequest).transaction;
|
|
103
|
+
|
|
104
|
+
if (!transaction) {
|
|
105
|
+
throw new Error("Transaction not available during upgrade");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let store: IDBObjectStore;
|
|
109
|
+
|
|
110
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
111
|
+
store = db.createObjectStore(this.storeName, { keyPath: "id" });
|
|
112
|
+
store.createIndex("start", "start", { unique: false });
|
|
113
|
+
store.createIndex("end", "end", { unique: false });
|
|
114
|
+
store.createIndex("calendar", "calendar", { unique: false });
|
|
115
|
+
store.createIndex("calendarSync", ["calendar", "lastSynced"], {
|
|
116
|
+
unique: false,
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
store = transaction.objectStore(this.storeName);
|
|
120
|
+
if (!store.indexNames.contains("calendar")) {
|
|
121
|
+
store.createIndex("calendar", "calendar", { unique: false });
|
|
122
|
+
}
|
|
123
|
+
if (!store.indexNames.contains("calendarSync")) {
|
|
124
|
+
store.createIndex("calendarSync", ["calendar", "lastSynced"], {
|
|
125
|
+
unique: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!db.objectStoreNames.contains(this.metadataStoreName)) {
|
|
131
|
+
db.createObjectStore(this.metadataStoreName, { keyPath: "sourceId" });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return this.openPromise;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async loadEvents(): Promise<CalendarEvent[]> {
|
|
140
|
+
const db = await this.open();
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const transaction = db.transaction(this.storeName, "readonly");
|
|
144
|
+
const store = transaction.objectStore(this.storeName);
|
|
145
|
+
const request = store.getAll();
|
|
146
|
+
|
|
147
|
+
request.onerror = () => {
|
|
148
|
+
reject(new Error(`Failed to load events: ${request.error?.message}`));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
request.onsuccess = () => {
|
|
152
|
+
const serializedEvents = request.result as SerializedEvent[];
|
|
153
|
+
const events = serializedEvents.map((e) => this.deserializeEvent(e));
|
|
154
|
+
resolve(events);
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async queryEvents(start: Date, end: Date): Promise<CalendarEvent[]> {
|
|
160
|
+
const db = await this.open();
|
|
161
|
+
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
const transaction = db.transaction(this.storeName, "readonly");
|
|
164
|
+
const store = transaction.objectStore(this.storeName);
|
|
165
|
+
const index = store.index("start");
|
|
166
|
+
|
|
167
|
+
const startTime = start.toISOString();
|
|
168
|
+
const endTime = end.toISOString();
|
|
169
|
+
|
|
170
|
+
// Get all events that start at or before the query end time
|
|
171
|
+
const range = IDBKeyRange.upperBound(endTime);
|
|
172
|
+
const request = index.getAll(range);
|
|
173
|
+
|
|
174
|
+
request.onerror = () => {
|
|
175
|
+
reject(new Error(`Failed to query events: ${request.error?.message}`));
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
request.onsuccess = () => {
|
|
179
|
+
const serializedEvents = request.result as SerializedEvent[];
|
|
180
|
+
|
|
181
|
+
// Filter to only events that end at or after the query start time
|
|
182
|
+
const overlappingEvents = serializedEvents.filter(
|
|
183
|
+
(e) => e.end >= startTime,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const events = overlappingEvents.map((e) => this.deserializeEvent(e));
|
|
187
|
+
resolve(events);
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async sync(
|
|
193
|
+
events: CalendarEvent[],
|
|
194
|
+
syncedSources: Map<string, Date>,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const db = await this.open();
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
200
|
+
const store = transaction.objectStore(this.storeName);
|
|
201
|
+
const calendarIndex = store.index("calendar");
|
|
202
|
+
|
|
203
|
+
// Upsert all new/updated events
|
|
204
|
+
for (const event of events) {
|
|
205
|
+
const serialized = this.serializeEvent(event);
|
|
206
|
+
store.put(serialized);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// For each synced source (including those with 0 events), delete stale events using cursor
|
|
210
|
+
for (const [calendar, syncTimestamp] of syncedSources.entries()) {
|
|
211
|
+
const latestSyncStr = syncTimestamp.toISOString();
|
|
212
|
+
const range = IDBKeyRange.only(calendar);
|
|
213
|
+
const cursorRequest = calendarIndex.openCursor(range);
|
|
214
|
+
|
|
215
|
+
cursorRequest.onsuccess = () => {
|
|
216
|
+
const cursor = cursorRequest.result;
|
|
217
|
+
if (cursor) {
|
|
218
|
+
const storedEvent = cursor.value as SerializedEvent;
|
|
219
|
+
// Delete if no lastSynced or if lastSynced is older than latest sync
|
|
220
|
+
if (
|
|
221
|
+
!storedEvent.lastSynced ||
|
|
222
|
+
storedEvent.lastSynced < latestSyncStr
|
|
223
|
+
) {
|
|
224
|
+
console.debug("sync deleted", storedEvent.id);
|
|
225
|
+
store.delete(storedEvent.id);
|
|
226
|
+
}
|
|
227
|
+
cursor.continue();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
cursorRequest.onerror = () => {
|
|
232
|
+
reject(
|
|
233
|
+
new Error(
|
|
234
|
+
`Failed to query calendar events: ${cursorRequest.error?.message}`,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
transaction.oncomplete = () => {
|
|
241
|
+
resolve();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
transaction.onerror = () => {
|
|
245
|
+
reject(
|
|
246
|
+
new Error(`Failed to save events: ${transaction.error?.message}`),
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async clear(): Promise<void> {
|
|
253
|
+
const db = await this.open();
|
|
254
|
+
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
257
|
+
const store = transaction.objectStore(this.storeName);
|
|
258
|
+
const request = store.clear();
|
|
259
|
+
|
|
260
|
+
request.onerror = () => {
|
|
261
|
+
reject(new Error(`Failed to clear storage: ${request.error?.message}`));
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
request.onsuccess = () => {
|
|
265
|
+
resolve();
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getSyncMetadata(sourceId: string): Promise<SyncMetadata | undefined> {
|
|
271
|
+
const db = await this.open();
|
|
272
|
+
|
|
273
|
+
if (!db.objectStoreNames.contains(this.metadataStoreName)) {
|
|
274
|
+
await this.recreateDatabase();
|
|
275
|
+
return this.getSyncMetadata(sourceId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const transaction = db.transaction(this.metadataStoreName, "readonly");
|
|
280
|
+
const store = transaction.objectStore(this.metadataStoreName);
|
|
281
|
+
const request = store.get(sourceId);
|
|
282
|
+
|
|
283
|
+
request.onerror = () => {
|
|
284
|
+
reject(
|
|
285
|
+
new Error(`Failed to get sync metadata: ${request.error?.message}`),
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
request.onsuccess = () => {
|
|
290
|
+
const serialized = request.result as SerializedMetadata | undefined;
|
|
291
|
+
if (!serialized) {
|
|
292
|
+
resolve(undefined);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
resolve({
|
|
297
|
+
lastSync: new Date(serialized.lastSync),
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async setSyncMetadata(
|
|
304
|
+
sourceId: string,
|
|
305
|
+
metadata: SyncMetadata,
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
const db = await this.open();
|
|
308
|
+
|
|
309
|
+
if (!db.objectStoreNames.contains(this.metadataStoreName)) {
|
|
310
|
+
await this.recreateDatabase();
|
|
311
|
+
return this.setSyncMetadata(sourceId, metadata);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const transaction = db.transaction(this.metadataStoreName, "readwrite");
|
|
316
|
+
const store = transaction.objectStore(this.metadataStoreName);
|
|
317
|
+
|
|
318
|
+
const serialized: SerializedMetadata = {
|
|
319
|
+
sourceId,
|
|
320
|
+
lastSync: metadata.lastSync.toISOString(),
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const request = store.put(serialized);
|
|
324
|
+
|
|
325
|
+
request.onerror = () => {
|
|
326
|
+
reject(
|
|
327
|
+
new Error(`Failed to set sync metadata: ${request.error?.message}`),
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
request.onsuccess = () => {
|
|
332
|
+
resolve();
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
serializeEvent(event: CalendarEvent): SerializedEvent {
|
|
338
|
+
// For all-day events, store as local date string (YYYY-MM-DD) to preserve the date
|
|
339
|
+
// For timed events, store as ISO string
|
|
340
|
+
const start = event.isAllDay
|
|
341
|
+
? `${event.start.getFullYear()}-${String(
|
|
342
|
+
event.start.getMonth() + 1,
|
|
343
|
+
).padStart(2, "0")}-${String(event.start.getDate()).padStart(2, "0")}`
|
|
344
|
+
: event.start.toISOString();
|
|
345
|
+
const end = event.isAllDay
|
|
346
|
+
? `${event.end.getFullYear()}-${String(event.end.getMonth() + 1).padStart(
|
|
347
|
+
2,
|
|
348
|
+
"0",
|
|
349
|
+
)}-${String(event.end.getDate()).padStart(2, "0")}`
|
|
350
|
+
: event.end.toISOString();
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
id: event.id,
|
|
354
|
+
title: event.title,
|
|
355
|
+
start,
|
|
356
|
+
end,
|
|
357
|
+
color: event.color,
|
|
358
|
+
calendar: event.calendar,
|
|
359
|
+
calendarId: event.calendarId,
|
|
360
|
+
sourceId: event.sourceId,
|
|
361
|
+
description: event.description,
|
|
362
|
+
location: event.location,
|
|
363
|
+
url: event.url,
|
|
364
|
+
rrule: event.rrule,
|
|
365
|
+
readOnly: event.readOnly,
|
|
366
|
+
lastSynced: event.lastSynced?.toISOString(),
|
|
367
|
+
organizer: event.organizer,
|
|
368
|
+
attendees: event.attendees,
|
|
369
|
+
status: event.status,
|
|
370
|
+
isAllDay: event.isAllDay,
|
|
371
|
+
reminders: event.reminders,
|
|
372
|
+
visualStyle: event.visualStyle,
|
|
373
|
+
resourceUrl: event.resourceUrl,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
deserializeEvent(serialized: SerializedEvent): CalendarEvent {
|
|
378
|
+
// Parse dates - for all-day events, parse as local date to avoid UTC shifts
|
|
379
|
+
let start: Date;
|
|
380
|
+
let end: Date;
|
|
381
|
+
|
|
382
|
+
if (serialized.isAllDay) {
|
|
383
|
+
// Parse as local date (YYYY-MM-DD format)
|
|
384
|
+
const startParts = serialized.start.split("-").map(Number);
|
|
385
|
+
if (
|
|
386
|
+
startParts.length === 3 &&
|
|
387
|
+
startParts.every((p) => !Number.isNaN(p))
|
|
388
|
+
) {
|
|
389
|
+
const [year, month, day] = startParts as [number, number, number];
|
|
390
|
+
start = new Date(year, month - 1, day);
|
|
391
|
+
} else {
|
|
392
|
+
start = new Date(serialized.start);
|
|
393
|
+
}
|
|
394
|
+
const endParts = serialized.end.split("-").map(Number);
|
|
395
|
+
if (endParts.length === 3 && endParts.every((p) => !Number.isNaN(p))) {
|
|
396
|
+
const [year, month, day] = endParts as [number, number, number];
|
|
397
|
+
end = new Date(year, month - 1, day);
|
|
398
|
+
} else {
|
|
399
|
+
end = new Date(serialized.end);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
start = new Date(serialized.start);
|
|
403
|
+
end = new Date(serialized.end);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
id: serialized.id,
|
|
408
|
+
title: serialized.title,
|
|
409
|
+
start,
|
|
410
|
+
end,
|
|
411
|
+
color: serialized.color,
|
|
412
|
+
calendar: serialized.calendar,
|
|
413
|
+
calendarId: serialized.calendarId,
|
|
414
|
+
sourceId: serialized.sourceId,
|
|
415
|
+
description: serialized.description,
|
|
416
|
+
location: serialized.location,
|
|
417
|
+
url: serialized.url,
|
|
418
|
+
rrule: serialized.rrule,
|
|
419
|
+
readOnly: serialized.readOnly,
|
|
420
|
+
lastSynced: serialized.lastSynced
|
|
421
|
+
? new Date(serialized.lastSynced)
|
|
422
|
+
: undefined,
|
|
423
|
+
organizer: serialized.organizer,
|
|
424
|
+
attendees: serialized.attendees,
|
|
425
|
+
status: serialized.status,
|
|
426
|
+
isAllDay: serialized.isAllDay,
|
|
427
|
+
reminders: serialized.reminders,
|
|
428
|
+
visualStyle: serialized.visualStyle,
|
|
429
|
+
resourceUrl: serialized.resourceUrl,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async putEvent(event: CalendarEvent): Promise<void> {
|
|
434
|
+
const db = await this.open();
|
|
435
|
+
|
|
436
|
+
return new Promise((resolve, reject) => {
|
|
437
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
438
|
+
const store = transaction.objectStore(this.storeName);
|
|
439
|
+
store.put(this.serializeEvent(event));
|
|
440
|
+
|
|
441
|
+
transaction.oncomplete = () => resolve();
|
|
442
|
+
transaction.onerror = () => reject(transaction.error);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async deleteEvent(id: string): Promise<void> {
|
|
447
|
+
const db = await this.open();
|
|
448
|
+
|
|
449
|
+
return new Promise((resolve, reject) => {
|
|
450
|
+
const transaction = db.transaction(this.storeName, "readwrite");
|
|
451
|
+
const store = transaction.objectStore(this.storeName);
|
|
452
|
+
store.delete(id);
|
|
453
|
+
|
|
454
|
+
transaction.oncomplete = () => resolve();
|
|
455
|
+
transaction.onerror = () => reject(transaction.error);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
409
458
|
}
|