joplin-plugin-my-calendar 1.2.3
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/CHANGELOG.md +99 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/index.js +3327 -0
- package/dist/manifest.json +28 -0
- package/dist/ui/calendar.js +1020 -0
- package/dist/ui/icsImport.js +518 -0
- package/dist/ui/mycalendar.css +724 -0
- package/manifest.json +28 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
// src/ui/calendar.js
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
// Ensure a single shared settings object across all UI scripts.
|
|
5
|
+
window.__mcUiSettings = window.__mcUiSettings || {
|
|
6
|
+
weekStart: undefined,
|
|
7
|
+
debug: undefined,
|
|
8
|
+
dayEventsRefreshMinutes: undefined,
|
|
9
|
+
showEventTimeline: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const uiSettings = window.__mcUiSettings;
|
|
13
|
+
|
|
14
|
+
let __mcHasUiSettings = false;
|
|
15
|
+
|
|
16
|
+
const MSG = Object.freeze({
|
|
17
|
+
UI_READY: 'uiReady',
|
|
18
|
+
UI_ACK: 'uiAck',
|
|
19
|
+
UI_SETTINGS: 'uiSettings',
|
|
20
|
+
REDRAW_MONTH: 'redrawMonth',
|
|
21
|
+
IMPORT_DONE: 'importDone',
|
|
22
|
+
IMPORT_ERROR: 'importError',
|
|
23
|
+
RANGE_EVENTS: 'rangeEvents',
|
|
24
|
+
SHOW_EVENTS: 'showEvents',
|
|
25
|
+
RANGE_ICS: 'rangeIcs',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function createUiLogger(prefix, outputBoxId) {
|
|
30
|
+
|
|
31
|
+
function appendToBox(args) {
|
|
32
|
+
if (!outputBoxId) return;
|
|
33
|
+
const box = document.getElementById(outputBoxId);
|
|
34
|
+
if (!box) return;
|
|
35
|
+
try {
|
|
36
|
+
const div = document.createElement('div');
|
|
37
|
+
div.textContent = args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
|
|
38
|
+
box.appendChild(div);
|
|
39
|
+
box.scrollTop = box.scrollHeight;
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function forwardToMain(level, args) {
|
|
46
|
+
try {
|
|
47
|
+
// Tolerance: forward only in debug mode
|
|
48
|
+
if (uiSettings.debug !== true) return;
|
|
49
|
+
|
|
50
|
+
const pm = window.webviewApi?.postMessage;
|
|
51
|
+
if (typeof pm !== 'function') return;
|
|
52
|
+
|
|
53
|
+
const safeArgs = (args || []).map(a => {
|
|
54
|
+
if (a && typeof a === 'object' && a.message && a.stack) {
|
|
55
|
+
return {__error: true, message: a.message, stack: a.stack};
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
if (typeof a === 'string') return a;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(a);
|
|
61
|
+
} catch {
|
|
62
|
+
return String(a);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
pm({name: 'uiLog', source: 'calendar', level, args: safeArgs});
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function write(consoleFn, args) {
|
|
74
|
+
if (args.length > 0 && typeof args[0] === 'string') {
|
|
75
|
+
const [msg, ...rest] = args;
|
|
76
|
+
consoleFn(`${prefix} ${msg}`, ...rest);
|
|
77
|
+
// the level is determined by consoleFn or by the method (see below)
|
|
78
|
+
} else {
|
|
79
|
+
consoleFn(prefix, ...args);
|
|
80
|
+
// the level is determined by consoleFn or by the method (see below)
|
|
81
|
+
}
|
|
82
|
+
appendToBox(args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
log: (...args) => {
|
|
87
|
+
write(console.log, args);
|
|
88
|
+
forwardToMain('log', args);
|
|
89
|
+
},
|
|
90
|
+
info: (...args) => {
|
|
91
|
+
write(console.info, args);
|
|
92
|
+
forwardToMain('info', args);
|
|
93
|
+
},
|
|
94
|
+
debug: (...args) => {
|
|
95
|
+
write(console.log, args);
|
|
96
|
+
forwardToMain('debug', args);
|
|
97
|
+
},
|
|
98
|
+
warn: (...args) => {
|
|
99
|
+
write(console.warn, args);
|
|
100
|
+
forwardToMain('warn', args);
|
|
101
|
+
},
|
|
102
|
+
error: (...args) => {
|
|
103
|
+
write(console.error, args);
|
|
104
|
+
forwardToMain('error', args);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Expose for unit tests; keep singleton across reloads
|
|
110
|
+
const uiLogger = window.__mcUiLogger || (window.__mcUiLogger = createUiLogger('[MyCalendar]', 'mc-log'));
|
|
111
|
+
|
|
112
|
+
// console.log('[MyCalendar][DBG][weekStart] uiSettings 3::', uiSettings);
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
function log(...args) {
|
|
116
|
+
if (uiSettings.debug !== true) return;
|
|
117
|
+
uiLogger.log(...args);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function applyDebugUI() {
|
|
121
|
+
const box = document.getElementById('mc-log');
|
|
122
|
+
if (box) box.style.display = (uiSettings.debug === true) ? '' : 'none';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
function mcRegisterOnMessage(handler) {
|
|
127
|
+
window.__mcMsgHandlers = window.__mcMsgHandlers || [];
|
|
128
|
+
window.__mcMsgHandlers.push(handler);
|
|
129
|
+
|
|
130
|
+
if (window.__mcMsgDispatcherInstalled) return;
|
|
131
|
+
window.__mcMsgDispatcherInstalled = true;
|
|
132
|
+
|
|
133
|
+
if (window.webviewApi?.onMessage) {
|
|
134
|
+
window.webviewApi.onMessage((ev) => {
|
|
135
|
+
const msg = (ev && ev.message) ? ev.message : ev;
|
|
136
|
+
for (const h of window.__mcMsgHandlers) {
|
|
137
|
+
try {
|
|
138
|
+
h(msg);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
uiLogger.error('handler error', e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function init() {
|
|
148
|
+
try {
|
|
149
|
+
log('init start');
|
|
150
|
+
|
|
151
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
152
|
+
|
|
153
|
+
function getWeekdayMeta() {
|
|
154
|
+
// JS Date.getDay(): Sun=0..Sat=6
|
|
155
|
+
// We keep the UI labels stable (short English) because the grid is compact;
|
|
156
|
+
// actual date formatting elsewhere is localized via toLocaleDateString.
|
|
157
|
+
if (uiSettings.weekStart === 'sunday') {
|
|
158
|
+
return [
|
|
159
|
+
{label: 'Sun', dow: 0},
|
|
160
|
+
{label: 'Mon', dow: 1},
|
|
161
|
+
{label: 'Tue', dow: 2},
|
|
162
|
+
{label: 'Wed', dow: 3},
|
|
163
|
+
{label: 'Thu', dow: 4},
|
|
164
|
+
{label: 'Fri', dow: 5},
|
|
165
|
+
{label: 'Sat', dow: 6},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Default: Monday week start
|
|
170
|
+
return [
|
|
171
|
+
{label: 'Mon', dow: 1},
|
|
172
|
+
{label: 'Tue', dow: 2},
|
|
173
|
+
{label: 'Wed', dow: 3},
|
|
174
|
+
{label: 'Thu', dow: 4},
|
|
175
|
+
{label: 'Fri', dow: 5},
|
|
176
|
+
{label: 'Sat', dow: 6},
|
|
177
|
+
{label: 'Sun', dow: 0},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Local North (00:00) in MS of the era
|
|
182
|
+
function localMidnightTs(d) {
|
|
183
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The first day of the month (locally)
|
|
187
|
+
function startOfMonthLocal(d) {
|
|
188
|
+
return new Date(d.getFullYear(), d.getMonth(), 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// shift months (locally) and normalization on the first day
|
|
192
|
+
function addMonthsLocal(dateLocal, delta) {
|
|
193
|
+
const d = new Date(dateLocal.getTime());
|
|
194
|
+
d.setMonth(d.getMonth() + delta);
|
|
195
|
+
return startOfMonthLocal(d);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Beginning 6-week mesh (Monday)-locally
|
|
199
|
+
function startOfCalendarGridLocal(current) {
|
|
200
|
+
const first = new Date(current.getFullYear(), current.getMonth(), 1);
|
|
201
|
+
|
|
202
|
+
// console.log('[MyCalendar][DBG][weekStart] uiSettings.weekStart 4::', uiSettings.weekStart);
|
|
203
|
+
|
|
204
|
+
const firstDayJs = (uiSettings.weekStart === 'sunday') ? 0 : 1; // Sun=0, Mon=1
|
|
205
|
+
const jsDow = first.getDay(); // Sun=0..Sat=6
|
|
206
|
+
const offset = (jsDow - firstDayJs + 7) % 7;
|
|
207
|
+
|
|
208
|
+
const start = new Date(first.getTime() - offset * DAY);
|
|
209
|
+
return new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// End of the grid (42 cells)
|
|
213
|
+
function endOfCalendarGridLocal(current) {
|
|
214
|
+
const s = startOfCalendarGridLocal(current);
|
|
215
|
+
return new Date(s.getTime() + 42 * DAY - 1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
const $toolbar = () => document.getElementById('mc-toolbar');
|
|
220
|
+
const $grid = () => document.getElementById('mc-grid');
|
|
221
|
+
const $elist = () => document.getElementById('mc-events-list');
|
|
222
|
+
const $dayLabel = () => document.getElementById('mc-events-day-label');
|
|
223
|
+
|
|
224
|
+
function setGridLoading(isLoading) {
|
|
225
|
+
const grid = $grid();
|
|
226
|
+
if (!grid) return;
|
|
227
|
+
grid.classList.toggle('mc-loading', !!isLoading);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function updateDayEventsHeader(dayStartTs) {
|
|
231
|
+
const el = $dayLabel();
|
|
232
|
+
if (!el) return;
|
|
233
|
+
|
|
234
|
+
const d = new Date(dayStartTs);
|
|
235
|
+
|
|
236
|
+
// Shows only "day + month" (localized by UI/system language)
|
|
237
|
+
el.textContent = d.toLocaleDateString(undefined, {
|
|
238
|
+
day: 'numeric',
|
|
239
|
+
month: 'long',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
let current = startOfMonthLocal(new Date());
|
|
245
|
+
let selectedDayUtc = localMidnightTs(new Date());
|
|
246
|
+
|
|
247
|
+
// Events received for the current range of calendar grid (42 days)
|
|
248
|
+
let gridEvents = [];
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
// Local device TZ
|
|
252
|
+
function monthLabel(d) {
|
|
253
|
+
return d.toLocaleString(undefined, {month: 'long', year: 'numeric'});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let dayEventsRefreshTimer = null;
|
|
257
|
+
|
|
258
|
+
function clearDayEventsRefreshTimer() {
|
|
259
|
+
if (dayEventsRefreshTimer) {
|
|
260
|
+
clearTimeout(dayEventsRefreshTimer);
|
|
261
|
+
dayEventsRefreshTimer = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function applyDayTimelineVisibility() {
|
|
266
|
+
const ul = document.getElementById('mc-events-list');
|
|
267
|
+
if (!ul) return;
|
|
268
|
+
|
|
269
|
+
const hidden = uiSettings.showEventTimeline === false;
|
|
270
|
+
ul.classList.toggle('mc-hide-timeline', hidden);
|
|
271
|
+
|
|
272
|
+
// Ensure immediate behavior on settings toggle without requiring a full re-render.
|
|
273
|
+
if (hidden) {
|
|
274
|
+
ul.querySelectorAll('.mc-event-timeline').forEach((el) => el.remove());
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function markPastDayEvents() {
|
|
279
|
+
const ul = document.getElementById('mc-events-list');
|
|
280
|
+
if (!ul) return;
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
|
|
283
|
+
ul.querySelectorAll('.mc-event').forEach((li) => {
|
|
284
|
+
const end = Number(li.dataset.endUtc || li.dataset.startUtc || '');
|
|
285
|
+
const isPast = Number.isFinite(end) && end < now;
|
|
286
|
+
li.classList.toggle('mc-event-past', isPast);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function scheduleDayEventsRefresh() {
|
|
291
|
+
clearDayEventsRefreshTimer();
|
|
292
|
+
if (document.hidden) return;
|
|
293
|
+
|
|
294
|
+
// When event timeline is hidden, skip scheduling these UI update timers.
|
|
295
|
+
// They depend on dayEventsRefreshMinutes and are only useful when timeline markers are shown.
|
|
296
|
+
if (uiSettings.showEventTimeline === false) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ul = document.getElementById('mc-events-list');
|
|
301
|
+
if (!ul) return;
|
|
302
|
+
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
let nextChange = Number.POSITIVE_INFINITY;
|
|
305
|
+
|
|
306
|
+
ul.querySelectorAll('.mc-event').forEach((li) => {
|
|
307
|
+
const end = Number(li.dataset.endUtc || li.dataset.startUtc || '');
|
|
308
|
+
if (!Number.isFinite(end)) return;
|
|
309
|
+
if (end >= now && end < nextChange) nextChange = end;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (!Number.isFinite(nextChange)) return;
|
|
313
|
+
|
|
314
|
+
const refreshMin = Number(uiSettings.dayEventsRefreshMinutes);
|
|
315
|
+
const fallbackMs = (Number.isFinite(refreshMin) && refreshMin > 0 ? refreshMin : 1) * 60 * 1000;
|
|
316
|
+
|
|
317
|
+
const delay = Math.max(1000, Math.min((nextChange - now) + 1000, fallbackMs));
|
|
318
|
+
|
|
319
|
+
dayEventsRefreshTimer = setTimeout(() => {
|
|
320
|
+
markPastDayEvents();
|
|
321
|
+
scheduleDayEventsRefresh();
|
|
322
|
+
}, delay);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Updates "current time" dot inside per-event 24h timelines (day list)
|
|
326
|
+
let dayNowTimelineTimer = null;
|
|
327
|
+
|
|
328
|
+
function clearDayNowTimelineTimer() {
|
|
329
|
+
if (dayNowTimelineTimer) {
|
|
330
|
+
clearTimeout(dayNowTimelineTimer);
|
|
331
|
+
dayNowTimelineTimer = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getDayEventsRefreshMs() {
|
|
336
|
+
const refreshMin = Number(uiSettings.dayEventsRefreshMinutes);
|
|
337
|
+
const minutes = (Number.isFinite(refreshMin) && refreshMin > 0) ? refreshMin : 1;
|
|
338
|
+
return minutes * 60 * 1000;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function clampPct(p) {
|
|
342
|
+
if (!Number.isFinite(p)) return 0;
|
|
343
|
+
return Math.max(0, Math.min(100, p));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function updateDayNowTimelineDot() {
|
|
347
|
+
if (uiSettings.showEventTimeline === false) return;
|
|
348
|
+
|
|
349
|
+
const ul = document.getElementById('mc-events-list');
|
|
350
|
+
if (!ul) return;
|
|
351
|
+
|
|
352
|
+
const dayStartUtc = Number(ul.dataset.dayStartUtc || '');
|
|
353
|
+
applyDayTimelineVisibility();
|
|
354
|
+
if (!Number.isFinite(dayStartUtc)) return;
|
|
355
|
+
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
const inDay = now >= dayStartUtc && now < (dayStartUtc + DAY);
|
|
358
|
+
const pct = Math.max(0, Math.min(100, ((now - dayStartUtc) / DAY) * 100));
|
|
359
|
+
|
|
360
|
+
ul.querySelectorAll('.mc-event-timeline-now').forEach((dot) => {
|
|
361
|
+
const el = /** @type {HTMLElement} */ (dot);
|
|
362
|
+
if (!inDay) {
|
|
363
|
+
el.style.display = 'none';
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
el.style.display = '';
|
|
367
|
+
el.style.left = pct + '%';
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function scheduleDayNowTimelineTick() {
|
|
372
|
+
clearDayNowTimelineTimer();
|
|
373
|
+
if (document.hidden) return;
|
|
374
|
+
|
|
375
|
+
if (uiSettings.showEventTimeline === false) return;
|
|
376
|
+
|
|
377
|
+
const delay = Math.max(1000, getDayEventsRefreshMs());
|
|
378
|
+
|
|
379
|
+
dayNowTimelineTimer = setTimeout(() => {
|
|
380
|
+
updateDayNowTimelineDot();
|
|
381
|
+
scheduleDayNowTimelineTick();
|
|
382
|
+
}, delay);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (window.webviewApi?.onMessage) {
|
|
386
|
+
mcRegisterOnMessage(onPluginMessage);
|
|
387
|
+
} else {
|
|
388
|
+
log('webviewApi.onMessage missing');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Connecting with backend.
|
|
392
|
+
if (window.webviewApi?.postMessage) {
|
|
393
|
+
applyDebugUI();
|
|
394
|
+
window.webviewApi.postMessage({name: 'uiReady'});
|
|
395
|
+
log('uiReady sent');
|
|
396
|
+
|
|
397
|
+
// Desktop/Mobile: panel can be hidden/shown without reloading the plugin backend.
|
|
398
|
+
// When UI becomes visible again, re-announce readiness so backend re-sends uiSettings.
|
|
399
|
+
let _uiReadyDebounce = 0;
|
|
400
|
+
|
|
401
|
+
function sendUiReadyAgain() {
|
|
402
|
+
clearTimeout(_uiReadyDebounce);
|
|
403
|
+
_uiReadyDebounce = setTimeout(() => {
|
|
404
|
+
try {
|
|
405
|
+
window.webviewApi?.postMessage?.({name: 'uiReady'});
|
|
406
|
+
} catch (_err) {
|
|
407
|
+
// ignore
|
|
408
|
+
}
|
|
409
|
+
}, 50);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
document.addEventListener('visibilitychange', () => {
|
|
413
|
+
if (!document.hidden) sendUiReadyAgain();
|
|
414
|
+
|
|
415
|
+
if (document.hidden) {
|
|
416
|
+
clearDayEventsRefreshTimer();
|
|
417
|
+
clearDayNowTimelineTimer();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
markPastDayEvents();
|
|
421
|
+
scheduleDayEventsRefresh();
|
|
422
|
+
updateDayNowTimelineDot();
|
|
423
|
+
scheduleDayNowTimelineTick();
|
|
424
|
+
});
|
|
425
|
+
window.addEventListener('focus', () => sendUiReadyAgain());
|
|
426
|
+
|
|
427
|
+
} else {
|
|
428
|
+
log('webviewApi.postMessage missing at init');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---- Backend readiness (webviewApi race fix) ----
|
|
432
|
+
// Sometimes on first Joplin start the panel DOM is ready earlier than webviewApi injection.
|
|
433
|
+
// Then initial uiReady/requestRangeEvents are lost until user clicks "Today".
|
|
434
|
+
function ensureBackendReady(cb) {
|
|
435
|
+
// shared flags across reloads
|
|
436
|
+
window.__mcBackendReady = window.__mcBackendReady || false;
|
|
437
|
+
window.__mcUiReadySent = window.__mcUiReadySent || false;
|
|
438
|
+
window.__mcOnMessageRegistered = window.__mcOnMessageRegistered || false;
|
|
439
|
+
|
|
440
|
+
const tryNow = () => {
|
|
441
|
+
const canPost = !!window.webviewApi?.postMessage;
|
|
442
|
+
const canOnMsg = !!window.webviewApi?.onMessage;
|
|
443
|
+
if (!canPost || !canOnMsg) return false;
|
|
444
|
+
|
|
445
|
+
if (!window.__mcOnMessageRegistered) {
|
|
446
|
+
mcRegisterOnMessage(onPluginMessage);
|
|
447
|
+
window.__mcOnMessageRegistered = true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!window.__mcUiReadySent) {
|
|
451
|
+
window.webviewApi.postMessage({name: 'uiReady'});
|
|
452
|
+
window.__mcUiReadySent = true;
|
|
453
|
+
log('uiReady sent');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
window.__mcBackendReady = true;
|
|
457
|
+
return true;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
if (window.__mcBackendReady && window.__mcUiReadySent && window.__mcOnMessageRegistered) {
|
|
461
|
+
if (cb) cb();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (tryNow()) {
|
|
466
|
+
if (cb) cb();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
log('backend not ready yet; waiting for webviewApi...');
|
|
471
|
+
let attempts = 0;
|
|
472
|
+
const timer = setInterval(() => {
|
|
473
|
+
attempts++;
|
|
474
|
+
if (tryNow()) {
|
|
475
|
+
clearInterval(timer);
|
|
476
|
+
if (cb) cb();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// ~5 seconds max
|
|
480
|
+
if (attempts >= 50) {
|
|
481
|
+
clearInterval(timer);
|
|
482
|
+
log('backend still not ready after waiting; will keep UI visible, user action may trigger later');
|
|
483
|
+
}
|
|
484
|
+
}, 100);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ---- Test hooks (enabled only in Jest/jsdom) ----
|
|
488
|
+
// Must be inside init() because many helpers are function-scoped here.
|
|
489
|
+
try {
|
|
490
|
+
if (typeof window !== 'undefined' && window.__mcTestMode === true) {
|
|
491
|
+
window.__mcTest = {
|
|
492
|
+
ensureBackendReady,
|
|
493
|
+
getDayEventsRefreshMs,
|
|
494
|
+
updateDayNowTimelineDot,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
} catch (_e) {
|
|
498
|
+
// ignore
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
function unwrapPluginMessage(msg) {
|
|
503
|
+
// Joplin sometimes wraps as { message: <payload> }
|
|
504
|
+
if (msg && typeof msg === 'object' && msg.message) return msg.message;
|
|
505
|
+
|
|
506
|
+
// Defensive: sometimes events can arrive without a name
|
|
507
|
+
if (msg && typeof msg === 'object' && !msg.name && Array.isArray(msg.events)) {
|
|
508
|
+
return {name: MSG.RANGE_EVENTS, events: msg.events};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return msg;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function onPluginMessage(msg) {
|
|
515
|
+
msg = unwrapPluginMessage(msg);
|
|
516
|
+
|
|
517
|
+
log('onMessage:', msg && msg.name ? msg.name : msg);
|
|
518
|
+
if (!msg || !msg.name) return;
|
|
519
|
+
|
|
520
|
+
const handlers = {
|
|
521
|
+
[MSG.UI_ACK]: () => {
|
|
522
|
+
log('uiAck received');
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
[MSG.UI_SETTINGS]: () => {
|
|
526
|
+
const prevWeekStart = uiSettings.weekStart;
|
|
527
|
+
|
|
528
|
+
const prevShowEventTimeline = uiSettings.showEventTimeline;
|
|
529
|
+
|
|
530
|
+
if (msg.weekStart === 'monday' || msg.weekStart === 'sunday') {
|
|
531
|
+
uiSettings.weekStart = msg.weekStart;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (typeof msg.debug === 'boolean') {
|
|
535
|
+
uiSettings.debug = msg.debug;
|
|
536
|
+
applyDebugUI();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (msg.dayEventsRefreshMinutes !== undefined) {
|
|
540
|
+
const v = Number(msg.dayEventsRefreshMinutes);
|
|
541
|
+
if (Number.isFinite(v) && v > 0) {
|
|
542
|
+
uiSettings.dayEventsRefreshMinutes = v;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (typeof msg.showEventTimeline === 'boolean') {
|
|
547
|
+
uiSettings.showEventTimeline = msg.showEventTimeline;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Apply immediately (no re-render required)
|
|
551
|
+
if (prevShowEventTimeline !== uiSettings.showEventTimeline) {
|
|
552
|
+
applyDayTimelineVisibility();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Recompute day-list UI markers and timers according to new settings
|
|
556
|
+
clearDayEventsRefreshTimer();
|
|
557
|
+
clearDayNowTimelineTimer();
|
|
558
|
+
markPastDayEvents();
|
|
559
|
+
updateDayNowTimelineDot();
|
|
560
|
+
scheduleDayNowTimelineTick();
|
|
561
|
+
scheduleDayEventsRefresh();
|
|
562
|
+
|
|
563
|
+
const weekStartChanged = prevWeekStart !== uiSettings.weekStart;
|
|
564
|
+
const firstSettingsArrived = !__mcHasUiSettings;
|
|
565
|
+
__mcHasUiSettings = true;
|
|
566
|
+
|
|
567
|
+
if (firstSettingsArrived || weekStartChanged) {
|
|
568
|
+
drawMonth();
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
[MSG.REDRAW_MONTH]: () => {
|
|
573
|
+
drawMonth();
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// --- ICS import complete (success or error) -> restart grid ---
|
|
577
|
+
[MSG.IMPORT_DONE]: () => {
|
|
578
|
+
log('import finished -> refreshing calendar grid');
|
|
579
|
+
gridEvents = [];
|
|
580
|
+
setGridLoading(true);
|
|
581
|
+
drawMonth();
|
|
582
|
+
},
|
|
583
|
+
[MSG.IMPORT_ERROR]: () => {
|
|
584
|
+
log('import finished -> refreshing calendar grid');
|
|
585
|
+
gridEvents = [];
|
|
586
|
+
setGridLoading(true);
|
|
587
|
+
drawMonth();
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
[MSG.RANGE_EVENTS]: () => {
|
|
591
|
+
log('got rangeEvents:', (msg.events || []).length);
|
|
592
|
+
gridEvents = msg.events || [];
|
|
593
|
+
|
|
594
|
+
setGridLoading(false);
|
|
595
|
+
|
|
596
|
+
paintGrid();
|
|
597
|
+
renderDayEvents(selectedDayUtc);
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
[MSG.SHOW_EVENTS]: () => {
|
|
601
|
+
log('got showEvents:', (msg.events || []).length);
|
|
602
|
+
renderDayEvents(msg.dateUtc);
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
[MSG.RANGE_ICS]: () => {
|
|
606
|
+
log('got ICS bytes:', (msg.ics || '').length);
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const handler = handlers[msg.name];
|
|
611
|
+
if (handler) handler();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (window.webviewApi?.onMessage) {
|
|
615
|
+
mcRegisterOnMessage(onPluginMessage);
|
|
616
|
+
|
|
617
|
+
} else {
|
|
618
|
+
log('webviewApi.onMessage missing');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function button(text, title, onClick) {
|
|
622
|
+
const b = document.createElement('button');
|
|
623
|
+
b.className = 'mc-btn';
|
|
624
|
+
b.type = 'button';
|
|
625
|
+
b.title = title;
|
|
626
|
+
b.textContent = text;
|
|
627
|
+
b.addEventListener('click', onClick);
|
|
628
|
+
b.classList.add('mc-calendar-nav-btn');
|
|
629
|
+
return b;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function renderToolbar() {
|
|
633
|
+
const root = $toolbar();
|
|
634
|
+
if (!root) return;
|
|
635
|
+
root.innerHTML = '';
|
|
636
|
+
const wrap = document.createElement('div');
|
|
637
|
+
wrap.className = 'mc-toolbar-inner';
|
|
638
|
+
|
|
639
|
+
const btnPrev = button('‹', 'Previous month', () => {
|
|
640
|
+
current = addMonthsLocal(current, -1);
|
|
641
|
+
drawMonth();
|
|
642
|
+
});
|
|
643
|
+
const btnToday = button('Today', 'Today', () => {
|
|
644
|
+
current = startOfMonthLocal(new Date());
|
|
645
|
+
selectedDayUtc = localMidnightTs(new Date());
|
|
646
|
+
drawMonth();
|
|
647
|
+
});
|
|
648
|
+
const btnNext = button('›', 'Next month', () => {
|
|
649
|
+
current = addMonthsLocal(current, +1);
|
|
650
|
+
drawMonth();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Month YYYY title.
|
|
654
|
+
const title = document.createElement('div');
|
|
655
|
+
title.className = 'mc-title';
|
|
656
|
+
title.textContent = monthLabel(current);
|
|
657
|
+
|
|
658
|
+
wrap.appendChild(btnPrev);
|
|
659
|
+
wrap.appendChild(btnToday);
|
|
660
|
+
wrap.appendChild(btnNext);
|
|
661
|
+
wrap.appendChild(title);
|
|
662
|
+
root.appendChild(wrap);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function drawMonth() {
|
|
666
|
+
if (!uiSettings.weekStart) {
|
|
667
|
+
log('drawMonth skipped: weekStart not set');
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
renderToolbar();
|
|
671
|
+
renderGridSkeleton();
|
|
672
|
+
|
|
673
|
+
setGridLoading(true);
|
|
674
|
+
|
|
675
|
+
const from = startOfCalendarGridLocal(current);
|
|
676
|
+
const to = endOfCalendarGridLocal(current);
|
|
677
|
+
|
|
678
|
+
requestMonthRangeWithRetry(from, to);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let rangeRequestTimer = null;
|
|
682
|
+
|
|
683
|
+
function requestMonthRangeWithRetry(from, to) {
|
|
684
|
+
// First request
|
|
685
|
+
ensureBackendReady(() => {
|
|
686
|
+
log('requestRange', from.toISOString(), '→', to.toISOString());
|
|
687
|
+
window.webviewApi.postMessage({
|
|
688
|
+
name: 'requestRangeEvents',
|
|
689
|
+
fromUtc: from.getTime(),
|
|
690
|
+
toUtc: to.getTime(),
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
//If for 1200ms did not come rageevents - repeat once
|
|
694
|
+
if (rangeRequestTimer) clearTimeout(rangeRequestTimer);
|
|
695
|
+
rangeRequestTimer = setTimeout(() => {
|
|
696
|
+
if (!Array.isArray(gridEvents) || gridEvents.length === 0) {
|
|
697
|
+
log('rangeEvents timeout - retrying once');
|
|
698
|
+
ensureBackendReady(() => {
|
|
699
|
+
window.webviewApi.postMessage({
|
|
700
|
+
name: 'requestRangeEvents',
|
|
701
|
+
fromUtc: from.getTime(),
|
|
702
|
+
toUtc: to.getTime(),
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}, 1200);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function renderGridSkeleton() {
|
|
710
|
+
const start = startOfCalendarGridLocal(current);
|
|
711
|
+
const todayTs = localMidnightTs(new Date());
|
|
712
|
+
const grid = $grid();
|
|
713
|
+
if (!grid) return;
|
|
714
|
+
grid.innerHTML = '';
|
|
715
|
+
|
|
716
|
+
// Loader overlay (accessibility-friendly)
|
|
717
|
+
const loader = document.createElement('div');
|
|
718
|
+
loader.className = 'mc-grid-loader';
|
|
719
|
+
loader.setAttribute('role', 'status');
|
|
720
|
+
loader.setAttribute('aria-live', 'polite');
|
|
721
|
+
loader.setAttribute('aria-label', 'Loading calendar');
|
|
722
|
+
loader.innerHTML = '<div class="mc-grid-spinner"></div>';
|
|
723
|
+
grid.appendChild(loader);
|
|
724
|
+
|
|
725
|
+
const head = document.createElement('div');
|
|
726
|
+
head.className = 'mc-grid-head';
|
|
727
|
+
for (const {label, dow} of getWeekdayMeta()) {
|
|
728
|
+
const c = document.createElement('div');
|
|
729
|
+
c.className = 'mc-grid-head-cell';
|
|
730
|
+
c.textContent = label;
|
|
731
|
+
c.dataset.dow = String(dow);
|
|
732
|
+
// Weekend header cells (Sat/Sun)
|
|
733
|
+
if (dow === 0 || dow === 6) c.classList.add('mc-weekend');
|
|
734
|
+
|
|
735
|
+
head.appendChild(c);
|
|
736
|
+
}
|
|
737
|
+
grid.appendChild(head);
|
|
738
|
+
|
|
739
|
+
const body = document.createElement('div');
|
|
740
|
+
body.className = 'mc-grid-body';
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
for (let i = 0; i < 42; i++) {
|
|
744
|
+
const cellDate = new Date(start);
|
|
745
|
+
cellDate.setDate(start.getDate() + i);
|
|
746
|
+
const cellTs = localMidnightTs(cellDate);
|
|
747
|
+
|
|
748
|
+
const cell = document.createElement('div');
|
|
749
|
+
cell.className = 'mc-cell';
|
|
750
|
+
cell.dataset.utc = String(cellTs);
|
|
751
|
+
|
|
752
|
+
const dow = cellDate.getDay(); // Sun=0..Sat=6
|
|
753
|
+
if (dow === 0 || dow === 6) cell.classList.add('mc-weekend');
|
|
754
|
+
|
|
755
|
+
const inThisMonth = cellDate.getMonth() === current.getMonth();
|
|
756
|
+
if (!inThisMonth) cell.classList.add('mc-out');
|
|
757
|
+
|
|
758
|
+
// Visually mute all days before today (including leading/trailing days
|
|
759
|
+
// from adjacent months shown in the 6-week grid).
|
|
760
|
+
if (cellTs < todayTs) cell.classList.add('mc-past');
|
|
761
|
+
|
|
762
|
+
if (selectedDayUtc === cellTs) cell.classList.add('mc-selected');
|
|
763
|
+
if (todayTs === cellTs) cell.classList.add('mc-today');
|
|
764
|
+
|
|
765
|
+
const n = document.createElement('div');
|
|
766
|
+
n.className = 'mc-daynum';
|
|
767
|
+
n.textContent = String(cellDate.getDate());
|
|
768
|
+
cell.appendChild(n);
|
|
769
|
+
|
|
770
|
+
const dots = document.createElement('div');
|
|
771
|
+
dots.className = 'mc-dots';
|
|
772
|
+
dots.dataset.utc = String(cellTs);
|
|
773
|
+
cell.appendChild(dots);
|
|
774
|
+
|
|
775
|
+
cell.addEventListener('click', () => {
|
|
776
|
+
selectedDayUtc = cellTs;
|
|
777
|
+
window.webviewApi?.postMessage?.({name: 'dateClick', dateUtc: selectedDayUtc});
|
|
778
|
+
renderDayEvents(selectedDayUtc);
|
|
779
|
+
paintSelection();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
body.appendChild(cell);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
grid.appendChild(body);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
function paintSelection() {
|
|
790
|
+
const body = document.querySelector('#mc-grid .mc-grid-body');
|
|
791
|
+
if (!body) return;
|
|
792
|
+
body.querySelectorAll('.mc-cell').forEach(c => c.classList.remove('mc-selected'));
|
|
793
|
+
const sel = body.querySelector(`.mc-cell[data-utc="${selectedDayUtc}"]`);
|
|
794
|
+
if (sel) sel.classList.add('mc-selected');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function fmtHM(ts, tz) {
|
|
798
|
+
try {
|
|
799
|
+
// tz expected like "America/Toronto"
|
|
800
|
+
if (tz) {
|
|
801
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
802
|
+
timeZone: tz,
|
|
803
|
+
hour: '2-digit',
|
|
804
|
+
minute: '2-digit',
|
|
805
|
+
hour12: false,
|
|
806
|
+
}).format(new Date(ts));
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
// ignore invalid tz and fallback
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// fallback: environment timezone
|
|
813
|
+
return new Date(ts).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', hour12: false});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Slice an event interval into a specific local-day interval.
|
|
817
|
+
// dayStartTs is epoch ms for local midnight of the day cell.
|
|
818
|
+
// Returns null if the event does not intersect the day.
|
|
819
|
+
function sliceEventForDay(ev, dayStartTs) {
|
|
820
|
+
const dayEndTs = dayStartTs + 24 * 3600 * 1000 - 1;
|
|
821
|
+
const evStart = ev.startUtc;
|
|
822
|
+
const evEnd = (ev.endUtc ?? ev.startUtc);
|
|
823
|
+
const segStart = Math.max(evStart, dayStartTs);
|
|
824
|
+
const segEnd = Math.min(evEnd, dayEndTs);
|
|
825
|
+
if (segEnd < segStart) return null;
|
|
826
|
+
return {startUtc: segStart, endUtc: segEnd};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function paintGrid() {
|
|
830
|
+
const body = document.querySelector('#mc-grid .mc-grid-body');
|
|
831
|
+
if (!body) return;
|
|
832
|
+
|
|
833
|
+
// Clean the previous indicators
|
|
834
|
+
// body.querySelectorAll('.mc-bars').forEach(b => b.innerHTML = '');
|
|
835
|
+
// body.querySelectorAll('.mc-count').forEach(c => {
|
|
836
|
+
// c.textContent = '';
|
|
837
|
+
// c.style.display = 'none';
|
|
838
|
+
// });
|
|
839
|
+
|
|
840
|
+
// Gather events by day (store per-day slices so multi-day events render correctly)
|
|
841
|
+
const byDay = new Map(); // dayStartTs (local midnight epoch ms) -> [{ ev, slice }]
|
|
842
|
+
for (const ev of gridEvents) {
|
|
843
|
+
const startDay = localMidnightTs(new Date(ev.startUtc));
|
|
844
|
+
const endDay = localMidnightTs(new Date((ev.endUtc ?? ev.startUtc)));
|
|
845
|
+
for (let ts = startDay; ts <= endDay; ts += 24 * 3600 * 1000) {
|
|
846
|
+
const slice = sliceEventForDay(ev, ts);
|
|
847
|
+
if (!slice) continue;
|
|
848
|
+
if (!byDay.has(ts)) byDay.set(ts, []);
|
|
849
|
+
byDay.get(ts).push({ev, slice});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Auxiliary: Get/create subsidiaries in a cell
|
|
854
|
+
function ensureParts(cell) {
|
|
855
|
+
// let bars = cell.querySelector(':scope > .mc-bars');
|
|
856
|
+
let badge = cell.querySelector('.mc-count');
|
|
857
|
+
// if (!bars) {
|
|
858
|
+
// bars = document.createElement('div');
|
|
859
|
+
// bars.className = 'mc-bars';
|
|
860
|
+
// cell.appendChild(bars);
|
|
861
|
+
// }
|
|
862
|
+
if (!badge) {
|
|
863
|
+
badge = document.createElement('div');
|
|
864
|
+
badge.className = 'mc-count';
|
|
865
|
+
cell.appendChild(badge);
|
|
866
|
+
}
|
|
867
|
+
// return {bars, badge};
|
|
868
|
+
return {badge};
|
|
869
|
+
// return {bars};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// To paint
|
|
873
|
+
byDay.forEach((events, dayUtc) => {
|
|
874
|
+
const cell = body.querySelector(`.mc-cell[data-utc="${dayUtc}"]`);
|
|
875
|
+
if (!cell) return;
|
|
876
|
+
|
|
877
|
+
// const {bars,badge} = ensureParts(cell);
|
|
878
|
+
const {badge} = ensureParts(cell);
|
|
879
|
+
// const {bars} = ensureParts(cell);
|
|
880
|
+
|
|
881
|
+
// Color Event indicators in the calendar grid
|
|
882
|
+
// const top = events.slice().sort((a, b) => a.slice.startUtc - b.slice.startUtc);
|
|
883
|
+
// for (const item of top) {
|
|
884
|
+
// const ev = item.ev;
|
|
885
|
+
// const bar = document.createElement('div');
|
|
886
|
+
// bar.className = 'mc-bar';
|
|
887
|
+
// if (ev.color) bar.style.background = ev.color;
|
|
888
|
+
// bars.appendChild(bar);
|
|
889
|
+
// }
|
|
890
|
+
|
|
891
|
+
// Counter in the upper right corner
|
|
892
|
+
badge.textContent = String(events.length);
|
|
893
|
+
badge.style.display = 'block';
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function renderDayEvents(dayStartUtc) {
|
|
898
|
+
clearDayEventsRefreshTimer();
|
|
899
|
+
clearDayNowTimelineTimer();
|
|
900
|
+
updateDayEventsHeader(dayStartUtc);
|
|
901
|
+
|
|
902
|
+
const ul = $elist();
|
|
903
|
+
if (!ul) return;
|
|
904
|
+
ul.innerHTML = '';
|
|
905
|
+
ul.dataset.dayStartUtc = String(dayStartUtc);
|
|
906
|
+
// const dayEndUtc = dayStartUtc + 24 * 3600 * 1000 - 1;
|
|
907
|
+
|
|
908
|
+
if (!Array.isArray(gridEvents) || gridEvents.length === 0) {
|
|
909
|
+
log('source EMPTY - gridEvents not ready yet');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const source = gridEvents;
|
|
913
|
+
log('source LENGTH', source.length);
|
|
914
|
+
// The event belongs to the day if the interval [Start, end] intersects [daystart, daynd]
|
|
915
|
+
|
|
916
|
+
const daySlices = [];
|
|
917
|
+
for (const ev of source) {
|
|
918
|
+
const slice = sliceEventForDay(ev, dayStartUtc);
|
|
919
|
+
if (!slice) continue;
|
|
920
|
+
daySlices.push({ev, slice});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
daySlices.sort((a, b) => a.slice.startUtc - b.slice.startUtc);
|
|
924
|
+
|
|
925
|
+
log('renderDayEvents', new Date(dayStartUtc).toISOString().slice(0, 10), 'count=', daySlices.length);
|
|
926
|
+
|
|
927
|
+
if (!daySlices.length) {
|
|
928
|
+
const li = document.createElement('li');
|
|
929
|
+
li.className = 'mc-empty';
|
|
930
|
+
li.textContent = 'There are no events';
|
|
931
|
+
ul.appendChild(li);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
for (const item of daySlices) {
|
|
936
|
+
const ev = item.ev;
|
|
937
|
+
const slice = item.slice;
|
|
938
|
+
const li = document.createElement('li');
|
|
939
|
+
li.className = 'mc-event';
|
|
940
|
+
const color = document.createElement('span');
|
|
941
|
+
color.className = 'mc-color';
|
|
942
|
+
color.style.background = ev.color || 'var(--mc-default-event-color)';
|
|
943
|
+
const title = document.createElement('span');
|
|
944
|
+
title.className = 'mc-title';
|
|
945
|
+
title.textContent = ev.title || '(without a title)';
|
|
946
|
+
const t = document.createElement('span');
|
|
947
|
+
t.className = 'mc-time';
|
|
948
|
+
const tz = ev.tz; // comes from plugin-side events
|
|
949
|
+
const label = (slice.endUtc !== slice.startUtc)
|
|
950
|
+
? `${fmtHM(slice.startUtc, tz)}–${fmtHM(slice.endUtc, tz)}`
|
|
951
|
+
: fmtHM(slice.startUtc, tz);
|
|
952
|
+
|
|
953
|
+
t.textContent = label;
|
|
954
|
+
li.appendChild(color);
|
|
955
|
+
li.appendChild(title);
|
|
956
|
+
li.appendChild(t);
|
|
957
|
+
|
|
958
|
+
// 24h timeline under the event (segment = event slice, dot = current time in day)
|
|
959
|
+
if (uiSettings.showEventTimeline !== false) {
|
|
960
|
+
const timeline = document.createElement('div');
|
|
961
|
+
timeline.className = 'mc-event-timeline';
|
|
962
|
+
|
|
963
|
+
const seg = document.createElement('div');
|
|
964
|
+
seg.className = 'mc-event-timeline-seg';
|
|
965
|
+
seg.style.background = ev.color || 'var(--mc-default-event-color)';
|
|
966
|
+
|
|
967
|
+
const startPct = clampPct(((slice.startUtc - dayStartUtc) / DAY) * 100);
|
|
968
|
+
const endPct = clampPct(((slice.endUtc - dayStartUtc) / DAY) * 100);
|
|
969
|
+
const left = Math.min(startPct, endPct);
|
|
970
|
+
const right = Math.max(startPct, endPct);
|
|
971
|
+
|
|
972
|
+
seg.style.left = left + '%';
|
|
973
|
+
seg.style.width = Math.max(0, (right - left)) + '%';
|
|
974
|
+
|
|
975
|
+
const nowDot = document.createElement('div');
|
|
976
|
+
nowDot.className = 'mc-event-timeline-now';
|
|
977
|
+
|
|
978
|
+
timeline.appendChild(seg);
|
|
979
|
+
timeline.appendChild(nowDot);
|
|
980
|
+
li.appendChild(timeline);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
li.addEventListener('click', () => {
|
|
984
|
+
window.webviewApi?.postMessage?.({name: 'openNote', id: ev.id});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
li.dataset.startUtc = String(slice.startUtc);
|
|
988
|
+
li.dataset.endUtc = String(slice.endUtc ?? slice.startUtc);
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
ul.appendChild(li);
|
|
992
|
+
|
|
993
|
+
// log('DAY ev.title=', ev.title, 'ev.tz=', ev.tz, 'startUtc=', ev.startUtc);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
updateDayNowTimelineDot();
|
|
997
|
+
scheduleDayNowTimelineTick();
|
|
998
|
+
|
|
999
|
+
markPastDayEvents();
|
|
1000
|
+
scheduleDayEventsRefresh();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Launch
|
|
1004
|
+
// Ensure backend handshake is not lost on first start
|
|
1005
|
+
ensureBackendReady(() => {
|
|
1006
|
+
});
|
|
1007
|
+
drawMonth();
|
|
1008
|
+
|
|
1009
|
+
log('init done');
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
console.error('[MyCalendar UI] init error', e);
|
|
1012
|
+
log('init error', e && e.message ? e.message : String(e));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Init check
|
|
1017
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
1018
|
+
else init();
|
|
1019
|
+
|
|
1020
|
+
})();
|