mysticals 0.5.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/cli.js +1709 -933
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createElement } from "react";
5
5
  import { render } from "ink";
6
6
 
7
7
  // package.json
8
- var version = "0.5.0";
8
+ var version = "0.7.0";
9
9
 
10
10
  // src/client.ts
11
11
  import { spawn } from "child_process";
@@ -210,7 +210,669 @@ import { z } from "zod";
210
210
  import { addMonths, parseISO as parseISO2, subMonths } from "date-fns";
211
211
 
212
212
  // ../../packages/core/src/sync/notify.ts
213
- import { format, isSameDay, parseISO } from "date-fns";
213
+ import { format as format2, isSameDay, parseISO } from "date-fns";
214
+
215
+ // ../../packages/core/src/i18n.ts
216
+ import { format } from "date-fns";
217
+ import { enUS } from "date-fns/locale/en-US";
218
+ import { uk as ukDates } from "date-fns/locale/uk";
219
+
220
+ // ../../packages/core/src/i18n/en.ts
221
+ var en = {
222
+ // ---- core: shared bits ----
223
+ "common.untitled": "Untitled",
224
+ "common.newEvent": "New Event",
225
+ "common.today": "Today",
226
+ "when.allDay": "all day",
227
+ "status.inMinutes": "in {n}m",
228
+ "status.at": "at {time}",
229
+ "meeting.videoCall": "video call",
230
+ "link.meet": "Join Google Meet",
231
+ "link.zoom": "Join Zoom",
232
+ "link.teams": "Join Teams",
233
+ "link.video": "Join call \xB7 {host}",
234
+ "link.map": "Open map",
235
+ // ---- core: repeat rules ----
236
+ "weekday.short.MO": "Mon",
237
+ "weekday.short.TU": "Tue",
238
+ "weekday.short.WE": "Wed",
239
+ "weekday.short.TH": "Thu",
240
+ "weekday.short.FR": "Fri",
241
+ "weekday.short.SA": "Sat",
242
+ "weekday.short.SU": "Sun",
243
+ "weekday.on.MO": "on Monday",
244
+ "weekday.on.TU": "on Tuesday",
245
+ "weekday.on.WE": "on Wednesday",
246
+ "weekday.on.TH": "on Thursday",
247
+ "weekday.on.FR": "on Friday",
248
+ "weekday.on.SA": "on Saturday",
249
+ "weekday.on.SU": "on Sunday",
250
+ "repeat.none": "Does not repeat",
251
+ "repeat.every.daily": "Every day",
252
+ "repeat.every.weekly": "Every week",
253
+ "repeat.every.monthly": "Every month",
254
+ "repeat.every.yearly": "Every year",
255
+ "repeat.everyN.daily": { one: "Every {n} day", other: "Every {n} days" },
256
+ "repeat.everyN.weekly": { one: "Every {n} week", other: "Every {n} weeks" },
257
+ "repeat.everyN.monthly": { one: "Every {n} month", other: "Every {n} months" },
258
+ "repeat.everyN.yearly": { one: "Every {n} year", other: "Every {n} years" },
259
+ "repeat.weeklyOn": "Every week {day}",
260
+ "repeat.monthlyOn": "Every month on day {d}",
261
+ "repeat.yearlyOn": "Every year on {date}",
262
+ "repeat.weekdays": "Every weekday (Mon\u2013Fri)",
263
+ "repeat.onDays": "{every} on {days}",
264
+ "repeat.onDay": "{every} on day {d}",
265
+ "repeat.onDate": "{every} on {date}",
266
+ "repeat.until": "until {date}",
267
+ "repeat.count": { one: "{n} time", other: "{n} times" },
268
+ "repeat.custom": "Custom rule ({rule})",
269
+ // ---- core: event form errors ----
270
+ "form.endBeforeStartDate": "End date is before start date",
271
+ "form.endAfterStart": "End must be after start",
272
+ "form.invalidEmail": "Invalid email: {list}",
273
+ "form.chooseAccount": "Choose an account",
274
+ "form.chooseCalendar": "Choose a calendar",
275
+ // ---- core: notifications ----
276
+ "notify.invite": "New invite",
277
+ "notify.changed": "Event changed",
278
+ "notify.cancelled": "Event cancelled",
279
+ "notify.many": { one: "{n} calendar update", other: "{n} calendar updates" },
280
+ // ---- desktop: menus, dialogs, update ----
281
+ "menu.file": "File",
282
+ "menu.newEvent": "New Event",
283
+ "menu.close": "Close Window",
284
+ "menu.quit": "Quit",
285
+ "menu.edit": "Edit",
286
+ "menu.undo": "Undo",
287
+ "menu.redo": "Redo",
288
+ "menu.cut": "Cut",
289
+ "menu.copy": "Copy",
290
+ "menu.paste": "Paste",
291
+ "menu.selectAll": "Select All",
292
+ "menu.view": "View",
293
+ "menu.today": "Today",
294
+ "menu.reload": "Reload",
295
+ "menu.devTools": "Toggle Developer Tools",
296
+ "menu.fullScreen": "Toggle Full Screen",
297
+ "menu.window": "Window",
298
+ "menu.minimize": "Minimize",
299
+ "menu.zoom": "Zoom",
300
+ "menu.front": "Bring All to Front",
301
+ "menu.openLink": "Open Link",
302
+ "menu.copyLink": "Copy Link",
303
+ "view.agenda": "Agenda",
304
+ "view.day": "Day",
305
+ "view.3day": "3 Days",
306
+ "view.week": "Week",
307
+ "view.month": "Month",
308
+ "error.noSecureStorage": "Secure storage is unavailable; refusing to store credentials",
309
+ "error.noSecureStorageLinux": "Secure storage is unavailable; refusing to store credentials. Install and unlock a Secret Service keyring (GNOME Keyring or KWallet), then restart Mysticals.",
310
+ "error.accountsTitle": "Mysticals could not load your accounts",
311
+ "error.accountsBody": "{file} could not be read:\n{error}\n\nThe file was not changed. Fix it or move it aside (Mysticals then starts with no accounts), and reopen the app.",
312
+ "update.available": "Mysticals {version} is available",
313
+ "update.clickToUpdate": "Click to update",
314
+ "update.clickToDownload": "Click to download",
315
+ // ---- desktop: app shell and toolbar ----
316
+ "common.undo": "Undo",
317
+ "common.cancel": "Cancel",
318
+ "common.close": "Close",
319
+ "common.save": "Save",
320
+ "common.delete": "Delete",
321
+ "common.edit": "Edit",
322
+ "app.loading": "Loading",
323
+ "app.loadingAccounts": "loading accounts\u2026",
324
+ "toast.moved": "Moved \u201C{title}\u201D",
325
+ "toast.movedAll": "Moved \u201C{title}\u201D (all events)",
326
+ "toast.movedFollowing": "Moved \u201C{title}\u201D and following",
327
+ "toast.moveFailed": "Couldn't move \u201C{title}\u201D: {error}",
328
+ "scope.move": "Move recurring event",
329
+ "toolbar.weekNo": "W{n}",
330
+ "toolbar.previous": "Previous",
331
+ "toolbar.next": "Next",
332
+ "toolbar.today": "today",
333
+ "toolbar.newEventHint": "New event (N)",
334
+ "toolbar.new": "+ new",
335
+ "toolbar.syncing": "Syncing",
336
+ "toolbar.firstSync": "first sync of {names}\u2026",
337
+ // ---- desktop: settings ----
338
+ "settings.title": "Settings",
339
+ "settings.sections": "Settings sections",
340
+ "settings.tab.accounts": "Accounts",
341
+ "settings.tab.themes": "Themes",
342
+ "settings.tab.language": "Language",
343
+ "settings.tab.sync": "Sync",
344
+ "settings.tab.privacy": "Privacy",
345
+ "settings.noAccounts": "No accounts yet.",
346
+ "settings.addAccount": "Add account\u2026",
347
+ "settings.theme": "Theme",
348
+ "theme.amber": "Phosphor amber",
349
+ "theme.mono": "Black & White",
350
+ "theme.toon": "Cartoon",
351
+ "settings.language.auto": "Automatic",
352
+ "settings.language.system": "system: {name}",
353
+ "settings.sync.note": "Accounts sync automatically every 2 minutes and when the app regains focus.",
354
+ "settings.sync.syncing": "Syncing\u2026",
355
+ "settings.sync.all": "Sync all",
356
+ "settings.sync.now": "Sync now",
357
+ "settings.sync.stateSyncing": "syncing",
358
+ "settings.sync.stateError": "error",
359
+ "settings.sync.stateOk": "ok",
360
+ "settings.sync.lastFailed": "Last sync failed: {error}",
361
+ "settings.reconnect": "Reconnect",
362
+ "settings.privacy.note": "Mysticals sends two anonymous events: when the app is first installed and when an account is added (provider only). Never emails, server addresses, calendars or events.",
363
+ "settings.privacy.policy": "Privacy policy",
364
+ "settings.privacy.share": "Share anonymous usage stats",
365
+ "settings.account.labelFor": "Label for {email}",
366
+ "settings.account.colourFor": "Colour for {email}",
367
+ "settings.account.remove": "Remove",
368
+ "settings.account.removeNote": "Removes local data and credentials for {email}. Nothing is deleted on the server.",
369
+ "settings.account.removeConfirm": "Remove account",
370
+ // ---- desktop: accounts ----
371
+ "common.back": "Back",
372
+ "common.decrease": "Decrease",
373
+ "common.increase": "Increase",
374
+ "accounts.personal": "Personal",
375
+ "accounts.work": "Work",
376
+ "accounts.title": "Add calendar account",
377
+ "accounts.isolated": "Each account is isolated: events and invitations are only sent from the account they belong to.",
378
+ "accounts.google.hint": "Sign in with your browser",
379
+ "accounts.caldav.hint": "Private Email, iCloud, Fastmail\u2026",
380
+ "accounts.manage": "Manage existing accounts\u2026",
381
+ "accounts.google.waiting": "Waiting for Google sign-in in your browser\u2026",
382
+ "accounts.google.finish": "Finish signing in there; Mysticals comes back on its own.",
383
+ "accounts.step.signedInGoogle": "Signed in with Google",
384
+ "accounts.step.connecting": "Connecting your account\u2026",
385
+ "accounts.step.loadingCalendars": "Loading calendars",
386
+ "accounts.step.signingIn": "Signing in to {host}\u2026",
387
+ "accounts.field.provider": "Provider",
388
+ "accounts.provider.custom": "Custom",
389
+ "accounts.field.server": "Server URL",
390
+ "accounts.field.serverHint": "Check your provider's docs if connection fails.",
391
+ "accounts.field.email": "Email",
392
+ "accounts.field.password": "App password",
393
+ "accounts.field.label": "Label",
394
+ "accounts.field.colour": "Colour",
395
+ "accounts.field.accountColour": "Account colour",
396
+ "accounts.add": "Add account",
397
+ // ---- desktop: event editor ----
398
+ "editor.title": "Title",
399
+ "editor.account": "Account",
400
+ "editor.chooseAccount": "Choose account\u2026",
401
+ "editor.calendar": "Calendar",
402
+ "editor.chooseCalendar": "Choose calendar\u2026",
403
+ "editor.accountFirst": "Choose an account first",
404
+ "editor.allDay": "All-day",
405
+ "editor.starts": "Starts",
406
+ "editor.ends": "Ends",
407
+ "editor.location": "Location",
408
+ "editor.addLocation": "Add location",
409
+ "editor.invitees": "Invitees",
410
+ "editor.removeInvitee": "Remove {email}",
411
+ "editor.addPeople": "Add people (optional)",
412
+ "editor.notes": "Notes",
413
+ "editor.addNotes": "Add notes",
414
+ "editor.createdIn": "Created in",
415
+ "editor.invitesFrom": "Invitations will be sent from",
416
+ "editor.chooseWhich": "Choose which account this event belongs to.",
417
+ "editor.saving": "Saving\u2026",
418
+ "editor.add": "Add Event",
419
+ "scope.changeRule": "Change the repeat rule for",
420
+ "scope.save": "Save recurring event",
421
+ // ---- desktop: repeat picker ----
422
+ "scope.one": "This event",
423
+ "scope.following": "This and following",
424
+ "scope.all": "All events",
425
+ "repeat.label": "Repeat",
426
+ "repeat.loading": "Loading\u2026",
427
+ "repeat.loadFailed": "Couldn\u2019t load the repeat rule; it stays as it is.",
428
+ "repeat.customOption": "Custom\u2026",
429
+ "repeat.every": "Every",
430
+ "repeat.everyAria": "Repeat every",
431
+ "repeat.unitAria": "Repeat unit",
432
+ "repeat.unit.daily": "days",
433
+ "repeat.unit.weekly": "weeks",
434
+ "repeat.unit.monthly": "months",
435
+ "repeat.unit.yearly": "years",
436
+ "repeat.on": "On",
437
+ "repeat.onAria": "Repeat on",
438
+ "repeat.stops": "Stops",
439
+ "repeat.stopsAria": "Repeat stops",
440
+ "repeat.never": "Never",
441
+ "repeat.endOnDate": "On date",
442
+ "repeat.after": "After",
443
+ "repeat.stopsOn": "Stops on",
444
+ "repeat.occurrences": "Occurrences",
445
+ "repeat.times": "times",
446
+ // ---- desktop: date picker and event details ----
447
+ "dtf.date": "{label} date",
448
+ "dtf.time": "{label} time",
449
+ "dtf.typed": "{label} time, HH:mm",
450
+ "rsvp.accept": "Accept",
451
+ "rsvp.maybe": "Maybe",
452
+ "rsvp.decline": "Decline",
453
+ "rsvp.reply": "Reply",
454
+ "rsvp.replyAs": "Reply as {email}",
455
+ "partstat.accepted": "accepted",
456
+ "partstat.tentative": "tentative",
457
+ "partstat.declined": "declined",
458
+ "partstat.needsAction": "needs action",
459
+ "details.deleted": "This event was deleted",
460
+ "details.deletedFrom": "It was removed from {calendar}, possibly on another device.",
461
+ "details.deletedFromCalendar": "It was removed from the calendar, possibly on another device.",
462
+ "details.calendarIn": "{calendar} in",
463
+ "details.organizer": "Organizer",
464
+ "details.invitees": "Invitees ({n})",
465
+ "details.you": "you",
466
+ "details.organizerTag": "organizer",
467
+ "details.confirmDelete": "Delete this event?",
468
+ "scope.delete": "Delete recurring event",
469
+ // ---- desktop: invites, mini month, reconnect ----
470
+ "invites.title": "Invitations",
471
+ "invites.empty": "No pending invitations",
472
+ "invites.aria": "Invitations ({n})",
473
+ "invites.hint": "Invitations (i)",
474
+ "invites.count": { one: "{n} invite", other: "{n} invites" },
475
+ "mini.prev": "Previous month",
476
+ "mini.next": "Next month",
477
+ "reauth.done": "{label} reconnected",
478
+ "reauth.title": "{label} is disconnected",
479
+ "reauth.google": "Google no longer accepts the sign-in for {email}. Sign in again to reconnect.",
480
+ "reauth.caldav": "The password for {email} was changed or revoked. Enter a new app password to reconnect.",
481
+ "reauth.stayVisible": "Your calendars stay visible from the last sync until then.",
482
+ "reauth.signingIn": "Signing in\u2026",
483
+ "reauth.later": "Later",
484
+ "reauth.signInAgain": "Sign in again",
485
+ // ---- desktop: status bar and sidebar ----
486
+ "sbar.until": "until {time}",
487
+ "sbar.next": "next:",
488
+ "sbar.noUpcoming": "no upcoming events",
489
+ "sbar.updateTo": "\u2191 Update to {version}",
490
+ "sbar.download": "\u2193 Download {version}",
491
+ "sbar.downloading": "Downloading {n}%",
492
+ "sbar.restarting": "Restarting\u2026",
493
+ "sbar.updateFailed": "\u2715 update failed \xB7 retry",
494
+ "sbar.mode.agenda": "agenda",
495
+ "sbar.mode.day": "day",
496
+ "sbar.mode.3day": "3day",
497
+ "sbar.mode.week": "week",
498
+ "sbar.mode.month": "month",
499
+ "sbar.syncing": "syncing {names}",
500
+ "sbar.disconnected": "\u2715 disconnected",
501
+ "sbar.syncError": "\u2715 sync error",
502
+ "sbar.key.new": "new",
503
+ "sbar.key.today": "today",
504
+ "sbar.key.invites": "invites",
505
+ "sidebar.syncError": "Sync error: {error}",
506
+ "sidebar.loading": "Loading calendars\u2026",
507
+ "sidebar.disconnected": "Disconnected",
508
+ "sidebar.reconnect": "reconnect",
509
+ "sidebar.loadFailed": "Couldn't load calendars",
510
+ "sidebar.retry": "retry",
511
+ "sidebar.readOnlyTitle": "Read-only",
512
+ "sidebar.readOnly": "read-only",
513
+ "sidebar.add": "Add calendar",
514
+ // ---- desktop: calendar grids and agenda ----
515
+ "month.more": "+{n} more",
516
+ "grid.allDay": "all-day",
517
+ "grid.collapseAllDay": "Collapse all-day events",
518
+ "grid.showMoreAllDay": { one: "Show {n} more all-day event", other: "Show {n} more all-day events" },
519
+ "dur.m": "{m}m",
520
+ "dur.h": "{h}h",
521
+ "dur.hm": "{h}h {m}m",
522
+ "agenda.ended": "ended {time}",
523
+ "agenda.endsIn": "now \xB7 ends in {n}m",
524
+ "agenda.starts": "starts {when}",
525
+ "agenda.nothing": "Nothing scheduled today.",
526
+ "agenda.allDone": "All done for today.",
527
+ "agenda.now": "\u25CF now",
528
+ "agenda.joinShort": "Join",
529
+ "agenda.noCalls": "No calls today. Enjoy the quiet.",
530
+ "agenda.fact.calendar": "calendar",
531
+ "agenda.fact.where": "where",
532
+ "agenda.fact.people": "people",
533
+ "agenda.fact.link": "link",
534
+ "agenda.people": { one: "{n} person", other: "{n} people" },
535
+ "agenda.join": "Join call",
536
+ "agenda.joinOpens": "Join opens {n} min before start",
537
+ "agenda.details": "Details"
538
+ };
539
+
540
+ // ../../packages/core/src/i18n/uk.ts
541
+ var uk = {
542
+ // ---- core: shared bits ----
543
+ "common.untitled": "\u0411\u0435\u0437 \u043D\u0430\u0437\u0432\u0438",
544
+ "common.newEvent": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F",
545
+ "common.today": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
546
+ "when.allDay": "\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
547
+ "status.inMinutes": "\u0437\u0430 {n} \u0445\u0432",
548
+ "status.at": "\u043E {time}",
549
+ "meeting.videoCall": "\u0432\u0456\u0434\u0435\u043E\u0434\u0437\u0432\u0456\u043D\u043E\u043A",
550
+ "link.meet": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Google Meet",
551
+ "link.zoom": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Zoom",
552
+ "link.teams": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Teams",
553
+ "link.video": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \xB7 {host}",
554
+ "link.map": "\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u0440\u0442\u0443",
555
+ // ---- core: repeat rules ----
556
+ "weekday.short.MO": "\u043F\u043D",
557
+ "weekday.short.TU": "\u0432\u0442",
558
+ "weekday.short.WE": "\u0441\u0440",
559
+ "weekday.short.TH": "\u0447\u0442",
560
+ "weekday.short.FR": "\u043F\u0442",
561
+ "weekday.short.SA": "\u0441\u0431",
562
+ "weekday.short.SU": "\u043D\u0434",
563
+ "weekday.on.MO": "\u0443 \u043F\u043E\u043D\u0435\u0434\u0456\u043B\u043E\u043A",
564
+ "weekday.on.TU": "\u0443 \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A",
565
+ "weekday.on.WE": "\u0443 \u0441\u0435\u0440\u0435\u0434\u0443",
566
+ "weekday.on.TH": "\u0443 \u0447\u0435\u0442\u0432\u0435\u0440",
567
+ "weekday.on.FR": "\u0443 \u043F\u02BC\u044F\u0442\u043D\u0438\u0446\u044E",
568
+ "weekday.on.SA": "\u0443 \u0441\u0443\u0431\u043E\u0442\u0443",
569
+ "weekday.on.SU": "\u0443 \u043D\u0435\u0434\u0456\u043B\u044E",
570
+ "repeat.none": "\u041D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044E\u0454\u0442\u044C\u0441\u044F",
571
+ "repeat.every.daily": "\u0429\u043E\u0434\u043D\u044F",
572
+ "repeat.every.weekly": "\u0429\u043E\u0442\u0438\u0436\u043D\u044F",
573
+ "repeat.every.monthly": "\u0429\u043E\u043C\u0456\u0441\u044F\u0446\u044F",
574
+ "repeat.every.yearly": "\u0429\u043E\u0440\u043E\u043A\u0443",
575
+ "repeat.everyN.daily": { one: "\u041A\u043E\u0436\u0435\u043D {n} \u0434\u0435\u043D\u044C", few: "\u041A\u043E\u0436\u043D\u0456 {n} \u0434\u043D\u0456", many: "\u041A\u043E\u0436\u043D\u0456 {n} \u0434\u043D\u0456\u0432", other: "\u041A\u043E\u0436\u043D\u0456 {n} \u0434\u043D\u044F" },
576
+ "repeat.everyN.weekly": { one: "\u041A\u043E\u0436\u0435\u043D {n} \u0442\u0438\u0436\u0434\u0435\u043D\u044C", few: "\u041A\u043E\u0436\u043D\u0456 {n} \u0442\u0438\u0436\u043D\u0456", many: "\u041A\u043E\u0436\u043D\u0456 {n} \u0442\u0438\u0436\u043D\u0456\u0432", other: "\u041A\u043E\u0436\u043D\u0456 {n} \u0442\u0438\u0436\u043D\u044F" },
577
+ "repeat.everyN.monthly": { one: "\u041A\u043E\u0436\u0435\u043D {n} \u043C\u0456\u0441\u044F\u0446\u044C", few: "\u041A\u043E\u0436\u043D\u0456 {n} \u043C\u0456\u0441\u044F\u0446\u0456", many: "\u041A\u043E\u0436\u043D\u0456 {n} \u043C\u0456\u0441\u044F\u0446\u0456\u0432", other: "\u041A\u043E\u0436\u043D\u0456 {n} \u043C\u0456\u0441\u044F\u0446\u044F" },
578
+ "repeat.everyN.yearly": { one: "\u041A\u043E\u0436\u0435\u043D {n} \u0440\u0456\u043A", few: "\u041A\u043E\u0436\u043D\u0456 {n} \u0440\u043E\u043A\u0438", many: "\u041A\u043E\u0436\u043D\u0456 {n} \u0440\u043E\u043A\u0456\u0432", other: "\u041A\u043E\u0436\u043D\u0456 {n} \u0440\u043E\u043A\u0443" },
579
+ "repeat.weeklyOn": "\u0429\u043E\u0442\u0438\u0436\u043D\u044F {day}",
580
+ "repeat.monthlyOn": "\u0429\u043E\u043C\u0456\u0441\u044F\u0446\u044F {d}-\u0433\u043E \u0447\u0438\u0441\u043B\u0430",
581
+ "repeat.yearlyOn": "\u0429\u043E\u0440\u043E\u043A\u0443 {date}",
582
+ "repeat.weekdays": "\u0423 \u0431\u0443\u0434\u043D\u0456 (\u043F\u043D\u2013\u043F\u0442)",
583
+ "repeat.onDays": "{every} \u0443 {days}",
584
+ "repeat.onDay": "{every} {d}-\u0433\u043E \u0447\u0438\u0441\u043B\u0430",
585
+ "repeat.onDate": "{every} {date}",
586
+ "repeat.until": "\u0434\u043E {date}",
587
+ "repeat.count": { one: "{n} \u0440\u0430\u0437", few: "{n} \u0440\u0430\u0437\u0438", many: "{n} \u0440\u0430\u0437\u0456\u0432", other: "{n} \u0440\u0430\u0437\u0443" },
588
+ "repeat.custom": "\u0412\u043B\u0430\u0441\u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E ({rule})",
589
+ // ---- core: event form errors ----
590
+ "form.endBeforeStartDate": "\u0414\u0430\u0442\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044F \u0440\u0430\u043D\u0456\u0448\u0430 \u0437\u0430 \u0434\u0430\u0442\u0443 \u043F\u043E\u0447\u0430\u0442\u043A\u0443",
591
+ "form.endAfterStart": "\u041A\u0456\u043D\u0435\u0446\u044C \u043C\u0430\u0454 \u0431\u0443\u0442\u0438 \u043F\u0456\u0437\u043D\u0456\u0448\u0435 \u0437\u0430 \u043F\u043E\u0447\u0430\u0442\u043E\u043A",
592
+ "form.invalidEmail": "\u041D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u0430 \u0430\u0434\u0440\u0435\u0441\u0430: {list}",
593
+ "form.chooseAccount": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
594
+ "form.chooseCalendar": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
595
+ // ---- core: notifications ----
596
+ "notify.invite": "\u041D\u043E\u0432\u0435 \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
597
+ "notify.changed": "\u041F\u043E\u0434\u0456\u044E \u0437\u043C\u0456\u043D\u0435\u043D\u043E",
598
+ "notify.cancelled": "\u041F\u043E\u0434\u0456\u044E \u0441\u043A\u0430\u0441\u043E\u0432\u0430\u043D\u043E",
599
+ "notify.many": { one: "{n} \u0437\u043C\u0456\u043D\u0430 \u0432 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456", few: "{n} \u0437\u043C\u0456\u043D\u0438 \u0432 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456", many: "{n} \u0437\u043C\u0456\u043D \u0443 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456", other: "{n} \u0437\u043C\u0456\u043D\u0438 \u0432 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456" },
600
+ // ---- desktop: menus, dialogs, update ----
601
+ "menu.file": "\u0424\u0430\u0439\u043B",
602
+ "menu.newEvent": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F",
603
+ "menu.close": "\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u0432\u0456\u043A\u043D\u043E",
604
+ "menu.quit": "\u0412\u0438\u0439\u0442\u0438",
605
+ "menu.edit": "\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u043D\u043D\u044F",
606
+ "menu.undo": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
607
+ "menu.redo": "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438",
608
+ "menu.cut": "\u0412\u0438\u0440\u0456\u0437\u0430\u0442\u0438",
609
+ "menu.copy": "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438",
610
+ "menu.paste": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u0438",
611
+ "menu.selectAll": "\u0412\u0438\u0434\u0456\u043B\u0438\u0442\u0438 \u0432\u0441\u0435",
612
+ "menu.view": "\u0412\u0438\u0433\u043B\u044F\u0434",
613
+ "menu.today": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
614
+ "menu.reload": "\u041F\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438",
615
+ "menu.devTools": "\u0406\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0438 \u0440\u043E\u0437\u0440\u043E\u0431\u043D\u0438\u043A\u0430",
616
+ "menu.fullScreen": "\u041F\u043E\u0432\u043D\u043E\u0435\u043A\u0440\u0430\u043D\u043D\u0438\u0439 \u0440\u0435\u0436\u0438\u043C",
617
+ "menu.window": "\u0412\u0456\u043A\u043D\u043E",
618
+ "menu.minimize": "\u0417\u0433\u043E\u0440\u043D\u0443\u0442\u0438",
619
+ "menu.zoom": "\u041C\u0430\u0441\u0448\u0442\u0430\u0431\u0443\u0432\u0430\u0442\u0438",
620
+ "menu.front": "\u0423\u0441\u0456 \u043D\u0430 \u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u043B\u0430\u043D",
621
+ "menu.openLink": "\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
622
+ "menu.copyLink": "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
623
+ "view.agenda": "\u0420\u043E\u0437\u043A\u043B\u0430\u0434",
624
+ "view.day": "\u0414\u0435\u043D\u044C",
625
+ "view.3day": "3 \u0434\u043D\u0456",
626
+ "view.week": "\u0422\u0438\u0436\u0434\u0435\u043D\u044C",
627
+ "view.month": "\u041C\u0456\u0441\u044F\u0446\u044C",
628
+ "error.noSecureStorage": "\u0417\u0430\u0445\u0438\u0449\u0435\u043D\u0435 \u0441\u0445\u043E\u0432\u0438\u0449\u0435 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0435; \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0434\u0430\u043D\u0456 \u043D\u0435 \u0431\u0443\u0434\u0435 \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E",
629
+ "error.noSecureStorageLinux": "\u0417\u0430\u0445\u0438\u0449\u0435\u043D\u0435 \u0441\u0445\u043E\u0432\u0438\u0449\u0435 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0435; \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0434\u0430\u043D\u0456 \u043D\u0435 \u0431\u0443\u0434\u0435 \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E. \u0412\u0441\u0442\u0430\u043D\u043E\u0432\u0456\u0442\u044C \u0456 \u0440\u043E\u0437\u0431\u043B\u043E\u043A\u0443\u0439\u0442\u0435 \u0441\u0445\u043E\u0432\u0438\u0449\u0435 \u043A\u043B\u044E\u0447\u0456\u0432 Secret Service (GNOME Keyring \u0430\u0431\u043E KWallet), \u043F\u043E\u0442\u0456\u043C \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0456\u0442\u044C Mysticals.",
630
+ "error.accountsTitle": "Mysticals \u043D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0437\u0430\u043F\u0438\u0441\u0438",
631
+ "error.accountsBody": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u0438 {file}:\n{error}\n\n\u0424\u0430\u0439\u043B \u043D\u0435 \u0437\u043C\u0456\u043D\u0435\u043D\u043E. \u0412\u0438\u043F\u0440\u0430\u0432\u0442\u0435 \u0439\u043E\u0433\u043E \u0430\u0431\u043E \u043F\u0435\u0440\u0435\u043C\u0456\u0441\u0442\u0456\u0442\u044C (\u0442\u043E\u0434\u0456 Mysticals \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C\u0441\u044F \u0431\u0435\u0437 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0445 \u0437\u0430\u043F\u0438\u0441\u0456\u0432) \u0456 \u0437\u043D\u043E\u0432\u0443 \u0432\u0456\u0434\u043A\u0440\u0438\u0439\u0442\u0435 \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043E\u043A.",
632
+ "update.available": "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0432\u0435\u0440\u0441\u0456\u044F Mysticals {version}",
633
+ "update.clickToUpdate": "\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C, \u0449\u043E\u0431 \u043E\u043D\u043E\u0432\u0438\u0442\u0438",
634
+ "update.clickToDownload": "\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C, \u0449\u043E\u0431 \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438",
635
+ // ---- desktop: app shell and toolbar ----
636
+ "common.undo": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
637
+ "common.cancel": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
638
+ "common.close": "\u0417\u0430\u043A\u0440\u0438\u0442\u0438",
639
+ "common.save": "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438",
640
+ "common.delete": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",
641
+ "common.edit": "\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438",
642
+ "app.loading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F",
643
+ "app.loadingAccounts": "\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0445 \u0437\u0430\u043F\u0438\u0441\u0456\u0432\u2026",
644
+ "toast.moved": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB",
645
+ "toast.movedAll": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB (\u0443\u0441\u0456 \u043F\u043E\u0434\u0456\u0457)",
646
+ "toast.movedFollowing": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB \u0456 \u043D\u0430\u0441\u0442\u0443\u043F\u043D\u0456",
647
+ "toast.moveFailed": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \xAB{title}\xBB: {error}",
648
+ "scope.move": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u043D\u0443 \u043F\u043E\u0434\u0456\u044E",
649
+ "toolbar.weekNo": "\u0422{n}",
650
+ "toolbar.previous": "\u041D\u0430\u0437\u0430\u0434",
651
+ "toolbar.next": "\u0412\u043F\u0435\u0440\u0435\u0434",
652
+ "toolbar.today": "\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
653
+ "toolbar.newEventHint": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F (N)",
654
+ "toolbar.new": "+ \u043D\u043E\u0432\u0430",
655
+ "toolbar.syncing": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
656
+ "toolbar.firstSync": "\u043F\u0435\u0440\u0448\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F: {names}\u2026",
657
+ // ---- desktop: settings ----
658
+ "settings.title": "\u041D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u043D\u044F",
659
+ "settings.sections": "\u0420\u043E\u0437\u0434\u0456\u043B\u0438 \u043D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u044C",
660
+ "settings.tab.accounts": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0437\u0430\u043F\u0438\u0441\u0438",
661
+ "settings.tab.themes": "\u0422\u0435\u043C\u0438",
662
+ "settings.tab.language": "\u041C\u043E\u0432\u0430",
663
+ "settings.tab.sync": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
664
+ "settings.tab.privacy": "\u041A\u043E\u043D\u0444\u0456\u0434\u0435\u043D\u0446\u0456\u0439\u043D\u0456\u0441\u0442\u044C",
665
+ "settings.noAccounts": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0445 \u0437\u0430\u043F\u0438\u0441\u0456\u0432 \u0449\u0435 \u043D\u0435\u043C\u0430\u0454.",
666
+ "settings.addAccount": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441\u2026",
667
+ "settings.theme": "\u0422\u0435\u043C\u0430",
668
+ "theme.amber": "\u0411\u0443\u0440\u0448\u0442\u0438\u043D\u043E\u0432\u0438\u0439 \u043B\u044E\u043C\u0456\u043D\u043E\u0444\u043E\u0440",
669
+ "theme.mono": "\u0427\u043E\u0440\u043D\u043E-\u0431\u0456\u043B\u0430",
670
+ "theme.toon": "\u041C\u0443\u043B\u044C\u0442\u044F\u0448\u043D\u0430",
671
+ "settings.language.auto": "\u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E",
672
+ "settings.language.system": "\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u0430: {name}",
673
+ "settings.sync.note": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0437\u0430\u043F\u0438\u0441\u0438 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0443\u044E\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u043A\u043E\u0436\u043D\u0456 2 \u0445\u0432\u0438\u043B\u0438\u043D\u0438 \u0442\u0430 \u043A\u043E\u043B\u0438 \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043E\u043A \u0437\u043D\u043E\u0432\u0443 \u0441\u0442\u0430\u0454 \u0430\u043A\u0442\u0438\u0432\u043D\u0438\u043C.",
674
+ "settings.sync.syncing": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F\u2026",
675
+ "settings.sync.all": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0441\u0435",
676
+ "settings.sync.now": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0443\u0432\u0430\u0442\u0438",
677
+ "settings.sync.stateSyncing": "\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
678
+ "settings.sync.stateError": "\u043F\u043E\u043C\u0438\u043B\u043A\u0430",
679
+ "settings.sync.stateOk": "\u043E\u043A",
680
+ "settings.sync.lastFailed": "\u041E\u0441\u0442\u0430\u043D\u043D\u044F \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F \u043D\u0435 \u0432\u0434\u0430\u043B\u0430\u0441\u044F: {error}",
681
+ "settings.reconnect": "\u041F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438 \u0437\u043D\u043E\u0432\u0443",
682
+ "settings.privacy.note": "Mysticals \u043D\u0430\u0434\u0441\u0438\u043B\u0430\u0454 \u0434\u0432\u0456 \u0430\u043D\u043E\u043D\u0456\u043C\u043D\u0456 \u043F\u043E\u0434\u0456\u0457: \u043A\u043E\u043B\u0438 \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043E\u043A \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u0432\u043F\u0435\u0440\u0448\u0435 \u0456 \u043A\u043E\u043B\u0438 \u0434\u043E\u0434\u0430\u043D\u043E \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441 (\u043B\u0438\u0448\u0435 \u0442\u0438\u043F \u0441\u0435\u0440\u0432\u0456\u0441\u0443). \u041D\u0456\u043A\u043E\u043B\u0438 \u043D\u0435 \u043D\u0430\u0434\u0441\u0438\u043B\u0430\u0454 \u0430\u0434\u0440\u0435\u0441\u0438 \u043F\u043E\u0448\u0442\u0438 \u0447\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432, \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456 \u0430\u0431\u043E \u043F\u043E\u0434\u0456\u0457.",
683
+ "settings.privacy.policy": "\u041F\u043E\u043B\u0456\u0442\u0438\u043A\u0430 \u043A\u043E\u043D\u0444\u0456\u0434\u0435\u043D\u0446\u0456\u0439\u043D\u043E\u0441\u0442\u0456",
684
+ "settings.privacy.share": "\u041D\u0430\u0434\u0441\u0438\u043B\u0430\u0442\u0438 \u0430\u043D\u043E\u043D\u0456\u043C\u043D\u0443 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0443 \u0432\u0438\u043A\u043E\u0440\u0438\u0441\u0442\u0430\u043D\u043D\u044F",
685
+ "settings.account.labelFor": "\u041D\u0430\u0437\u0432\u0430 \u0434\u043B\u044F {email}",
686
+ "settings.account.colourFor": "\u041A\u043E\u043B\u0456\u0440 \u0434\u043B\u044F {email}",
687
+ "settings.account.remove": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",
688
+ "settings.account.removeNote": "\u0412\u0438\u0434\u0430\u043B\u044F\u0454 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0456 \u0434\u0430\u043D\u0456 \u0442\u0430 \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u0456 \u0434\u0430\u043D\u0456 \u0432\u0445\u043E\u0434\u0443 \u0434\u043B\u044F {email}. \u041D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u043D\u0456\u0447\u043E\u0433\u043E \u043D\u0435 \u0432\u0438\u0434\u0430\u043B\u044F\u0454\u0442\u044C\u0441\u044F.",
689
+ "settings.account.removeConfirm": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
690
+ // ---- desktop: accounts ----
691
+ "common.back": "\u041D\u0430\u0437\u0430\u0434",
692
+ "common.decrease": "\u0417\u043C\u0435\u043D\u0448\u0438\u0442\u0438",
693
+ "common.increase": "\u0417\u0431\u0456\u043B\u044C\u0448\u0438\u0442\u0438",
694
+ "accounts.personal": "\u041E\u0441\u043E\u0431\u0438\u0441\u0442\u0438\u0439",
695
+ "accounts.work": "\u0420\u043E\u0431\u043E\u0447\u0438\u0439",
696
+ "accounts.title": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044F",
697
+ "accounts.isolated": "\u041A\u043E\u0436\u0435\u043D \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441 \u0432\u0456\u0434\u043E\u043A\u0440\u0435\u043C\u043B\u0435\u043D\u0438\u0439: \u043F\u043E\u0434\u0456\u0457 \u0442\u0430 \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F \u043D\u0430\u0434\u0441\u0438\u043B\u0430\u044E\u0442\u044C\u0441\u044F \u043B\u0438\u0448\u0435 \u0437 \u0442\u043E\u0433\u043E \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443, \u044F\u043A\u043E\u043C\u0443 \u0432\u043E\u043D\u0438 \u043D\u0430\u043B\u0435\u0436\u0430\u0442\u044C.",
698
+ "accounts.google.hint": "\u0412\u0445\u0456\u0434 \u0447\u0435\u0440\u0435\u0437 \u0431\u0440\u0430\u0443\u0437\u0435\u0440",
699
+ "accounts.caldav.hint": "Private Email, iCloud, Fastmail\u2026",
700
+ "accounts.manage": "\u041A\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043D\u0430\u044F\u0432\u043D\u0438\u043C\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u043C\u0438 \u0437\u0430\u043F\u0438\u0441\u0430\u043C\u0438\u2026",
701
+ "accounts.google.waiting": "\u041E\u0447\u0456\u043A\u0443\u0454\u043C\u043E \u043D\u0430 \u0432\u0445\u0456\u0434 \u0443 Google \u0447\u0435\u0440\u0435\u0437 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u2026",
702
+ "accounts.google.finish": "\u0417\u0430\u0432\u0435\u0440\u0448\u0456\u0442\u044C \u0432\u0445\u0456\u0434 \u0442\u0430\u043C; Mysticals \u043F\u043E\u0432\u0435\u0440\u043D\u0435\u0442\u044C\u0441\u044F \u0441\u0430\u043C.",
703
+ "accounts.step.signedInGoogle": "\u0412\u0445\u0456\u0434 \u0443 Google \u0432\u0438\u043A\u043E\u043D\u0430\u043D\u043E",
704
+ "accounts.step.connecting": "\u041F\u0456\u0434\u043A\u043B\u044E\u0447\u0430\u0454\u043C\u043E \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441\u2026",
705
+ "accounts.step.loadingCalendars": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0443\u0454\u043C\u043E \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456",
706
+ "accounts.step.signingIn": "\u0412\u0445\u0456\u0434 \u043D\u0430 {host}\u2026",
707
+ "accounts.field.provider": "\u0421\u0435\u0440\u0432\u0456\u0441",
708
+ "accounts.provider.custom": "\u0406\u043D\u0448\u0438\u0439",
709
+ "accounts.field.server": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430",
710
+ "accounts.field.serverHint": "\u042F\u043A\u0449\u043E \u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438\u0441\u044F \u043D\u0435 \u0432\u0434\u0430\u0454\u0442\u044C\u0441\u044F, \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u043D\u044C\u0442\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0456\u044E \u0441\u0435\u0440\u0432\u0456\u0441\u0443.",
711
+ "accounts.field.email": "\u0415\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0430 \u043F\u043E\u0448\u0442\u0430",
712
+ "accounts.field.password": "\u041F\u0430\u0440\u043E\u043B\u044C \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043A\u0443",
713
+ "accounts.field.label": "\u041D\u0430\u0437\u0432\u0430",
714
+ "accounts.field.colour": "\u041A\u043E\u043B\u0456\u0440",
715
+ "accounts.field.accountColour": "\u041A\u043E\u043B\u0456\u0440 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443",
716
+ "accounts.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
717
+ // ---- desktop: event editor ----
718
+ "editor.title": "\u041D\u0430\u0437\u0432\u0430",
719
+ "editor.account": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
720
+ "editor.chooseAccount": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441\u2026",
721
+ "editor.calendar": "\u041A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
722
+ "editor.chooseCalendar": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u2026",
723
+ "editor.accountFirst": "\u0421\u043F\u0435\u0440\u0448\u0443 \u043E\u0431\u0435\u0440\u0456\u0442\u044C \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
724
+ "editor.allDay": "\u0423\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
725
+ "editor.starts": "\u041F\u043E\u0447\u0430\u0442\u043E\u043A",
726
+ "editor.ends": "\u041A\u0456\u043D\u0435\u0446\u044C",
727
+ "editor.location": "\u041C\u0456\u0441\u0446\u0435",
728
+ "editor.addLocation": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043C\u0456\u0441\u0446\u0435",
729
+ "editor.invitees": "\u0423\u0447\u0430\u0441\u043D\u0438\u043A\u0438",
730
+ "editor.removeInvitee": "\u041F\u0440\u0438\u0431\u0440\u0430\u0442\u0438 {email}",
731
+ "editor.addPeople": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043B\u044E\u0434\u0435\u0439 (\u043D\u0435\u043E\u0431\u043E\u0432\u02BC\u044F\u0437\u043A\u043E\u0432\u043E)",
732
+ "editor.notes": "\u041D\u043E\u0442\u0430\u0442\u043A\u0438",
733
+ "editor.addNotes": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043D\u043E\u0442\u0430\u0442\u043A\u0438",
734
+ "editor.createdIn": "\u0421\u0442\u0432\u043E\u0440\u044E\u0454\u0442\u044C\u0441\u044F \u0432",
735
+ "editor.invitesFrom": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F \u0431\u0443\u0434\u0435 \u043D\u0430\u0434\u0456\u0441\u043B\u0430\u043D\u043E \u0437",
736
+ "editor.chooseWhich": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C, \u0434\u043E \u044F\u043A\u043E\u0433\u043E \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443 \u043D\u0430\u043B\u0435\u0436\u0438\u0442\u044C \u043F\u043E\u0434\u0456\u044F.",
737
+ "editor.saving": "\u0417\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043D\u044F\u2026",
738
+ "editor.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043F\u043E\u0434\u0456\u044E",
739
+ "scope.changeRule": "\u0417\u043C\u0456\u043D\u0438\u0442\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043F\u043E\u0432\u0442\u043E\u0440\u0435\u043D\u043D\u044F:",
740
+ "scope.save": "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u043D\u0443 \u043F\u043E\u0434\u0456\u044E",
741
+ // ---- desktop: repeat picker ----
742
+ "scope.one": "\u0426\u044F \u043F\u043E\u0434\u0456\u044F",
743
+ "scope.following": "\u0426\u044F \u0439 \u043D\u0430\u0441\u0442\u0443\u043F\u043D\u0456",
744
+ "scope.all": "\u0423\u0441\u0456 \u043F\u043E\u0434\u0456\u0457",
745
+ "repeat.label": "\u041F\u043E\u0432\u0442\u043E\u0440",
746
+ "repeat.loading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F\u2026",
747
+ "repeat.loadFailed": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043F\u043E\u0432\u0442\u043E\u0440\u0435\u043D\u043D\u044F; \u0432\u043E\u043D\u043E \u043B\u0438\u0448\u0438\u0442\u044C\u0441\u044F \u0431\u0435\u0437 \u0437\u043C\u0456\u043D.",
748
+ "repeat.customOption": "\u0406\u043D\u0448\u0435\u2026",
749
+ "repeat.every": "\u0420\u0430\u0437 \u043D\u0430",
750
+ "repeat.everyAria": "\u041F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u0442\u0438 \u043A\u043E\u0436\u043D\u0456",
751
+ "repeat.unitAria": "\u041E\u0434\u0438\u043D\u0438\u0446\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u0443",
752
+ "repeat.unit.daily": { one: "\u0434\u0435\u043D\u044C", few: "\u0434\u043D\u0456", many: "\u0434\u043D\u0456\u0432", other: "\u0434\u043D\u044F" },
753
+ "repeat.unit.weekly": { one: "\u0442\u0438\u0436\u0434\u0435\u043D\u044C", few: "\u0442\u0438\u0436\u043D\u0456", many: "\u0442\u0438\u0436\u043D\u0456\u0432", other: "\u0442\u0438\u0436\u043D\u044F" },
754
+ "repeat.unit.monthly": { one: "\u043C\u0456\u0441\u044F\u0446\u044C", few: "\u043C\u0456\u0441\u044F\u0446\u0456", many: "\u043C\u0456\u0441\u044F\u0446\u0456\u0432", other: "\u043C\u0456\u0441\u044F\u0446\u044F" },
755
+ "repeat.unit.yearly": { one: "\u0440\u0456\u043A", few: "\u0440\u043E\u043A\u0438", many: "\u0440\u043E\u043A\u0456\u0432", other: "\u0440\u043E\u043A\u0443" },
756
+ "repeat.on": "\u0414\u043D\u0456",
757
+ "repeat.onAria": "\u041F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u0442\u0438 \u0432 \u0434\u043D\u0456",
758
+ "repeat.stops": "\u0417\u0430\u043A\u0456\u043D\u0447\u0435\u043D\u043D\u044F",
759
+ "repeat.stopsAria": "\u041A\u043E\u043B\u0438 \u0437\u0430\u043A\u0456\u043D\u0447\u0438\u0442\u0438 \u043F\u043E\u0432\u0442\u043E\u0440",
760
+ "repeat.never": "\u041D\u0456\u043A\u043E\u043B\u0438",
761
+ "repeat.endOnDate": "\u0414\u0430\u0442\u0430",
762
+ "repeat.after": "\u041F\u0456\u0441\u043B\u044F",
763
+ "repeat.stopsOn": "\u0417\u0430\u043A\u0456\u043D\u0447\u0443\u0454\u0442\u044C\u0441\u044F",
764
+ "repeat.occurrences": "\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0456\u0432",
765
+ "repeat.times": { one: "\u0440\u0430\u0437", few: "\u0440\u0430\u0437\u0438", many: "\u0440\u0430\u0437\u0456\u0432", other: "\u0440\u0430\u0437\u0443" },
766
+ // ---- desktop: date picker and event details ----
767
+ "dtf.date": "{label}: \u0434\u0430\u0442\u0430",
768
+ "dtf.time": "{label}: \u0447\u0430\u0441",
769
+ "dtf.typed": "{label}: \u0447\u0430\u0441, \u0413\u0413:\u0425\u0425",
770
+ "rsvp.accept": "\u041F\u0440\u0438\u0439\u043D\u044F\u0442\u0438",
771
+ "rsvp.maybe": "\u041C\u043E\u0436\u043B\u0438\u0432\u043E",
772
+ "rsvp.decline": "\u0412\u0456\u0434\u0445\u0438\u043B\u0438\u0442\u0438",
773
+ "rsvp.reply": "\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C",
774
+ "rsvp.replyAs": "\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u0432\u0456\u0434 {email}",
775
+ "partstat.accepted": "\u043F\u0440\u0438\u0439\u043D\u044F\u0442\u043E",
776
+ "partstat.tentative": "\u043C\u043E\u0436\u043B\u0438\u0432\u043E",
777
+ "partstat.declined": "\u0432\u0456\u0434\u0445\u0438\u043B\u0435\u043D\u043E",
778
+ "partstat.needsAction": "\u0431\u0435\u0437 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u0456",
779
+ "details.deleted": "\u0426\u044E \u043F\u043E\u0434\u0456\u044E \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u043E",
780
+ "details.deletedFrom": "\u0407\u0457 \u043F\u0440\u0438\u0431\u0440\u0430\u043B\u0438 \u0437 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044F \xAB{calendar}\xBB, \u043C\u043E\u0436\u043B\u0438\u0432\u043E, \u043D\u0430 \u0456\u043D\u0448\u043E\u043C\u0443 \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u0457.",
781
+ "details.deletedFromCalendar": "\u0407\u0457 \u043F\u0440\u0438\u0431\u0440\u0430\u043B\u0438 \u0437 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u044F, \u043C\u043E\u0436\u043B\u0438\u0432\u043E, \u043D\u0430 \u0456\u043D\u0448\u043E\u043C\u0443 \u043F\u0440\u0438\u0441\u0442\u0440\u043E\u0457.",
782
+ "details.calendarIn": "{calendar} \u0443",
783
+ "details.organizer": "\u041E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440",
784
+ "details.invitees": "\u0423\u0447\u0430\u0441\u043D\u0438\u043A\u0438 ({n})",
785
+ "details.you": "\u0432\u0438",
786
+ "details.organizerTag": "\u043E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440",
787
+ "details.confirmDelete": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0446\u044E \u043F\u043E\u0434\u0456\u044E?",
788
+ "scope.delete": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u043D\u0443 \u043F\u043E\u0434\u0456\u044E",
789
+ // ---- desktop: invites, mini month, reconnect ----
790
+ "invites.title": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
791
+ "invites.empty": "\u041D\u0435\u043C\u0430\u0454 \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u044C \u0431\u0435\u0437 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u0456",
792
+ "invites.aria": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F ({n})",
793
+ "invites.hint": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F (i)",
794
+ "invites.count": { one: "{n} \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F", few: "{n} \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F", many: "{n} \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u044C", other: "{n} \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F" },
795
+ "mini.prev": "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C",
796
+ "mini.next": "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C",
797
+ "reauth.done": "{label}: \u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0437\u043D\u043E\u0432\u0443",
798
+ "reauth.title": "{label}: \u043F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0438\u0439 \u0432\u0445\u0456\u0434",
799
+ "reauth.google": "Google \u0431\u0456\u043B\u044C\u0448\u0435 \u043D\u0435 \u043F\u0440\u0438\u0439\u043C\u0430\u0454 \u0432\u0445\u0456\u0434 \u0434\u043B\u044F {email}. \u0423\u0432\u0456\u0439\u0434\u0456\u0442\u044C \u0437\u043D\u043E\u0432\u0443, \u0449\u043E\u0431 \u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438\u0441\u044F.",
800
+ "reauth.caldav": "\u041F\u0430\u0440\u043E\u043B\u044C \u0434\u043B\u044F {email} \u0437\u043C\u0456\u043D\u0435\u043D\u043E \u0430\u0431\u043E \u0432\u0456\u0434\u043A\u043B\u0438\u043A\u0430\u043D\u043E. \u0412\u0432\u0435\u0434\u0456\u0442\u044C \u043D\u043E\u0432\u0438\u0439 \u043F\u0430\u0440\u043E\u043B\u044C \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043A\u0443, \u0449\u043E\u0431 \u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438\u0441\u044F.",
801
+ "reauth.stayVisible": "\u0414\u043E \u0442\u043E\u0433\u043E \u0447\u0430\u0441\u0443 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043E \u0441\u0442\u0430\u043D\u043E\u043C \u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043D\u044E \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044E.",
802
+ "reauth.signingIn": "\u0412\u0445\u0456\u0434\u2026",
803
+ "reauth.later": "\u041F\u0456\u0437\u043D\u0456\u0448\u0435",
804
+ "reauth.signInAgain": "\u0423\u0432\u0456\u0439\u0442\u0438 \u0437\u043D\u043E\u0432\u0443",
805
+ // ---- desktop: status bar and sidebar ----
806
+ "sbar.until": "\u0434\u043E {time}",
807
+ "sbar.next": "\u0434\u0430\u043B\u0456:",
808
+ "sbar.noUpcoming": "\u043D\u0430\u0439\u0431\u043B\u0438\u0436\u0447\u0438\u0445 \u043F\u043E\u0434\u0456\u0439 \u043D\u0435\u043C\u0430\u0454",
809
+ "sbar.updateTo": "\u2191 \u041E\u043D\u043E\u0432\u0438\u0442\u0438 \u0434\u043E {version}",
810
+ "sbar.download": "\u2193 \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 {version}",
811
+ "sbar.downloading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F {n}%",
812
+ "sbar.restarting": "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u2026",
813
+ "sbar.updateFailed": "\u2715 \u043E\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044F \u043D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \xB7 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438",
814
+ "sbar.mode.agenda": "\u0440\u043E\u0437\u043A\u043B\u0430\u0434",
815
+ "sbar.mode.day": "\u0434\u0435\u043D\u044C",
816
+ "sbar.mode.3day": "3 \u0434\u043D\u0456",
817
+ "sbar.mode.week": "\u0442\u0438\u0436\u0434\u0435\u043D\u044C",
818
+ "sbar.mode.month": "\u043C\u0456\u0441\u044F\u0446\u044C",
819
+ "sbar.syncing": "\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F: {names}",
820
+ "sbar.disconnected": "\u2715 \u043F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u0432\u0445\u0456\u0434",
821
+ "sbar.syncError": "\u2715 \u043F\u043E\u043C\u0438\u043B\u043A\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u0457",
822
+ "sbar.key.new": "\u043D\u043E\u0432\u0430",
823
+ "sbar.key.today": "\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
824
+ "sbar.key.invites": "\u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
825
+ "sidebar.syncError": "\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u0457: {error}",
826
+ "sidebar.loading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456\u0432\u2026",
827
+ "sidebar.disconnected": "\u041F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0438\u0439 \u0432\u0445\u0456\u0434",
828
+ "sidebar.reconnect": "\u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438",
829
+ "sidebar.loadFailed": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456",
830
+ "sidebar.retry": "\u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438",
831
+ "sidebar.readOnlyTitle": "\u041B\u0438\u0448\u0435 \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u0434",
832
+ "sidebar.readOnly": "\u043B\u0438\u0448\u0435 \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u0434",
833
+ "sidebar.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
834
+ // ---- desktop: calendar grids and agenda ----
835
+ "month.more": "\u0449\u0435 {n}",
836
+ "grid.allDay": "\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
837
+ "grid.collapseAllDay": "\u0417\u0433\u043E\u0440\u043D\u0443\u0442\u0438 \u043F\u043E\u0434\u0456\u0457 \u043D\u0430 \u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
838
+ "grid.showMoreAllDay": { one: "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0449\u0435 {n} \u043F\u043E\u0434\u0456\u044E \u043D\u0430 \u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C", few: "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0449\u0435 {n} \u043F\u043E\u0434\u0456\u0457 \u043D\u0430 \u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C", many: "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0449\u0435 {n} \u043F\u043E\u0434\u0456\u0439 \u043D\u0430 \u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C", other: "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0449\u0435 {n} \u043F\u043E\u0434\u0456\u0457 \u043D\u0430 \u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C" },
839
+ "dur.m": "{m} \u0445\u0432",
840
+ "dur.h": "{h} \u0433\u043E\u0434",
841
+ "dur.hm": "{h} \u0433\u043E\u0434 {m} \u0445\u0432",
842
+ "agenda.ended": "\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044F \u043E {time}",
843
+ "agenda.endsIn": "\u0437\u0430\u0440\u0430\u0437 \xB7 \u0434\u043E \u043A\u0456\u043D\u0446\u044F {n} \u0445\u0432",
844
+ "agenda.starts": "\u043F\u043E\u0447\u0438\u043D\u0430\u0454\u0442\u044C\u0441\u044F {when}",
845
+ "agenda.nothing": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456 \u043D\u0456\u0447\u043E\u0433\u043E \u043D\u0435 \u0437\u0430\u043F\u043B\u0430\u043D\u043E\u0432\u0430\u043D\u043E.",
846
+ "agenda.allDone": "\u041D\u0430 \u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456 \u0432\u0441\u0435.",
847
+ "agenda.now": "\u25CF \u0437\u0430\u0440\u0430\u0437",
848
+ "agenda.joinShort": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F",
849
+ "agenda.noCalls": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456 \u0434\u0437\u0432\u0456\u043D\u043A\u0456\u0432 \u043D\u0435\u043C\u0430\u0454. \u041D\u0430\u0441\u043E\u043B\u043E\u0434\u0436\u0443\u0439\u0442\u0435\u0441\u044F \u0442\u0438\u0448\u0435\u044E.",
850
+ "agenda.fact.calendar": "\u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
851
+ "agenda.fact.where": "\u0434\u0435",
852
+ "agenda.fact.people": "\u0443\u0447\u0430\u0441\u043D\u0438\u043A\u0438",
853
+ "agenda.fact.link": "\u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
854
+ "agenda.people": { one: "{n} \u043B\u044E\u0434\u0438\u043D\u0430", few: "{n} \u043B\u044E\u0434\u0438\u043D\u0438", many: "{n} \u043B\u044E\u0434\u0435\u0439", other: "{n} \u043B\u044E\u0434\u0438\u043D\u0438" },
855
+ "agenda.join": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E \u0434\u0437\u0432\u0456\u043D\u043A\u0430",
856
+ "agenda.joinOpens": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u043C\u043E\u0436\u043D\u0430 \u0437\u0430 {n} \u0445\u0432 \u0434\u043E \u043F\u043E\u0447\u0430\u0442\u043A\u0443",
857
+ "agenda.details": "\u0414\u043E\u043A\u043B\u0430\u0434\u043D\u0456\u0448\u0435"
858
+ };
859
+
860
+ // ../../packages/core/src/i18n.ts
861
+ var DICT = { en, uk };
862
+ var plurals = /* @__PURE__ */ new Map();
863
+ var pluralOf = (locale, n) => {
864
+ if (!plurals.has(locale)) plurals.set(locale, new Intl.PluralRules(locale));
865
+ return plurals.get(locale).select(n);
866
+ };
867
+ function t(locale, key3, params) {
868
+ const msg = DICT[locale][key3] ?? en[key3];
869
+ const text2 = typeof msg === "string" ? msg : msg[pluralOf(locale, Number(params?.n ?? 0))] ?? msg.other;
870
+ return params ? text2.replace(/\{(\w+)\}/g, (m, k) => k in params ? String(params[k]) : m) : text2;
871
+ }
872
+ var dateLocale = (locale) => locale === "uk" ? ukDates : enUS;
873
+ var fmt = (locale, date, pattern) => format(date, locale === "uk" ? pattern.replace(/(?<!E)EEE(?!E)/g, "EEEEEE") : pattern, { locale: dateLocale(locale) });
874
+
875
+ // ../../packages/core/src/sync/notify.ts
214
876
  var at = (iso2, allDay) => (allDay ? parseISO(iso2) : new Date(iso2)).getTime();
215
877
  var key = (e) => `${e.calendarId}/${e.id}`;
216
878
  var WATCHED = ["title", "start", "end", "location"];
@@ -229,15 +891,18 @@ function diffEvents(prev, next, now = /* @__PURE__ */ new Date()) {
229
891
  for (const e of prev) if (live(e) && !after.has(key(e))) notes.push({ kind: "cancelled", event: e });
230
892
  return notes;
231
893
  }
232
- var TITLE = { invite: "New invite", changed: "Event changed", cancelled: "Event cancelled" };
233
- function noteText(notes, account, now = /* @__PURE__ */ new Date()) {
234
- if (notes.length > 3) return [{ title: `${notes.length} calendar updates`, body: account }];
235
- return notes.map(({ kind, event: e }) => ({ title: `${TITLE[kind]}: ${e.title || "Untitled"}`, body: `${when(e, now)} \xB7 ${account}` }));
894
+ var TITLE = { invite: "notify.invite", changed: "notify.changed", cancelled: "notify.cancelled" };
895
+ function noteText(notes, account, now = /* @__PURE__ */ new Date(), locale = "en") {
896
+ if (notes.length > 3) return [{ title: t(locale, "notify.many", { n: notes.length }), body: account }];
897
+ return notes.map(({ kind, event: e }) => ({
898
+ title: `${t(locale, TITLE[kind])}: ${e.title || t(locale, "common.untitled")}`,
899
+ body: `${when(e, now, locale)} \xB7 ${account}`
900
+ }));
236
901
  }
237
- function when(e, now) {
902
+ function when(e, now, locale) {
238
903
  const s = e.allDay ? parseISO(e.start) : new Date(e.start);
239
- if (e.allDay) return `${format(s, "EEE, d MMM")} \xB7 all day`;
240
- return `${isSameDay(s, now) ? "Today" : format(s, "EEE, d MMM")} \xB7 ${format(s, "HH:mm")}`;
904
+ if (e.allDay) return `${fmt(locale, s, "EEE, d MMM")} \xB7 ${t(locale, "when.allDay")}`;
905
+ return `${isSameDay(s, now) ? t(locale, "common.today") : fmt(locale, s, "EEE, d MMM")} \xB7 ${format2(s, "HH:mm")}`;
241
906
  }
242
907
 
243
908
  // ../../packages/core/src/providers/http.ts
@@ -292,7 +957,7 @@ var SyncEngine = class {
292
957
  this.running = false;
293
958
  this.unsubscribe?.();
294
959
  this.unsubscribe = void 0;
295
- for (const t of this.timers.values()) clearTimeout(t);
960
+ for (const t2 of this.timers.values()) clearTimeout(t2);
296
961
  this.timers.clear();
297
962
  }
298
963
  /**
@@ -465,7 +1130,7 @@ function startTelemetry(o) {
465
1130
  } catch {
466
1131
  return void 0;
467
1132
  }
468
- const host = env.MYSTICALS_TELEMETRY_HOST || HOST;
1133
+ const host2 = env.MYSTICALS_TELEMETRY_HOST || HOST;
469
1134
  const doFetch = o.fetch ?? fetch;
470
1135
  const track = (event, props) => {
471
1136
  if (o.allowed && !o.allowed()) return;
@@ -479,7 +1144,7 @@ function startTelemetry(o) {
479
1144
  $geoip_disable: true
480
1145
  };
481
1146
  try {
482
- doFetch(`${host}/i/v0/e/`, {
1147
+ doFetch(`${host2}/i/v0/e/`, {
483
1148
  method: "POST",
484
1149
  headers: { "Content-Type": "application/json" },
485
1150
  body: JSON.stringify({ api_key: o.key, event, distinct_id: install.id, properties }),
@@ -494,89 +1159,626 @@ function startTelemetry(o) {
494
1159
  return { track, installed: install.fresh };
495
1160
  }
496
1161
 
497
- // ../../packages/core/src/api.ts
498
- var PALETTE = ["#bd93f9", "#50fa7b", "#8be9fd", "#ff79c6", "#ffb86c", "#f1fa8c"];
499
- var id = z.string().min(1).max(1024);
500
- var isoDateOrTime = z.union([z.iso.datetime({ offset: true }), z.iso.date()]);
501
- var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
502
- var text = (max2) => z.string().max(max2);
503
- var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
504
- var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
505
- var Recurrence = z.object({
506
- freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
507
- interval: z.number().int().min(1).max(999).optional(),
508
- byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
509
- until: z.iso.date().optional(),
510
- count: z.number().int().min(1).max(999).optional(),
511
- // An RRULE value kept from the provider: one line of plain RRULE parts only.
512
- rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
513
- }).strict();
514
- var NewEvent = z.object({
515
- accountId: id,
516
- calendarId: id,
517
- title: text(1e3),
518
- start: isoDateOrTime,
519
- end: isoDateOrTime,
520
- allDay: z.boolean(),
521
- location: text(1e3).optional(),
522
- description: text(2e4).optional(),
523
- attendees: z.array(z.email()).max(500).optional(),
524
- recurrence: Recurrence.optional()
525
- }).refine(ordered, "end before start");
526
- var Attendee = z.object({
527
- email: text(320).min(1),
528
- name: text(500).optional(),
529
- status: z.enum(["accepted", "declined", "tentative", "needsAction"]),
530
- self: z.boolean().optional(),
531
- organizer: z.boolean().optional()
532
- });
533
- var EventRef = z.object({ id, accountId: id, calendarId: id });
534
- var EventEdit = EventRef.extend({
535
- title: text(1e3),
536
- start: isoDateOrTime,
537
- end: isoDateOrTime,
538
- allDay: z.boolean(),
539
- location: text(1e3).optional(),
540
- description: text(2e4).optional(),
541
- attendees: z.array(Attendee).max(500),
542
- // Absent keeps the series' rule; null stops repeating.
543
- recurrence: Recurrence.nullish()
544
- }).refine(ordered, "end before start");
545
- var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
546
- var CaldavInput = z.object({
547
- label: text(200).min(1),
548
- serverUrl: z.string().refine((u) => {
549
- if (!URL.canParse(u)) return false;
550
- const { protocol, hostname } = new URL(u);
551
- return protocol === "https:" || protocol === "http:" && LOOPBACK.has(hostname);
552
- }, "server URL must be https"),
553
- username: text(500).min(1),
554
- password: text(2e3).min(1),
555
- color: color.optional()
556
- });
557
- var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
558
- var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
559
- var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
560
- var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
561
- function createApi(store, sync, deps) {
562
- const account = (accountId) => {
563
- const a = store.get(id.parse(accountId));
564
- if (!a) throw new Error("unknown account");
565
- return a;
566
- };
567
- const syncOne = (accountId) => {
568
- sync.syncNow(accountId, { quiet: true, fresh: true }).catch((e) => console.error(`sync ${accountId} failed`, e));
569
- };
570
- const applyLocal = (accountId, fn) => {
571
- try {
572
- store.patchCache(accountId, (c) => ({ ...c, events: fn(c.events) }));
573
- sync.edited(accountId);
574
- deps.onChanged?.(accountId);
575
- } catch (e) {
576
- console.error(`local update of ${accountId} failed`, e);
1162
+ // ../../packages/core/src/logic/recurrence.ts
1163
+ import { format as format3, isValid, parseISO as parseISO3 } from "date-fns";
1164
+ var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
1165
+ var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
1166
+ var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
1167
+ var FREQS = ["daily", "weekly", "monthly", "yearly"];
1168
+ var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
1169
+ var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
1170
+ var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
1171
+ var shortDay = (d, locale = "en") => t(locale, `weekday.short.${d}`);
1172
+ function normalize(r, start) {
1173
+ if (r.rule) return { freq: r.freq, rule: r.rule };
1174
+ const out = { freq: r.freq };
1175
+ if (r.interval && r.interval > 1) out.interval = r.interval;
1176
+ if (r.freq === "weekly" && r.byDay?.length) {
1177
+ const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
1178
+ if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
1179
+ }
1180
+ if (r.until) out.until = r.until;
1181
+ else if (r.count) out.count = r.count;
1182
+ return out;
1183
+ }
1184
+ function sameRecurrence(a, b, start) {
1185
+ if (!a || !b) return a === b;
1186
+ return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
1187
+ }
1188
+ function toRRule(r, allDay) {
1189
+ if (r.rule) return r.rule;
1190
+ const parts = [`FREQ=${r.freq.toUpperCase()}`];
1191
+ if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
1192
+ if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
1193
+ if (r.until) {
1194
+ const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
1195
+ parts.push(`UNTIL=${until}`);
1196
+ } else if (r.count) parts.push(`COUNT=${r.count}`);
1197
+ return parts.join(";");
1198
+ }
1199
+ var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
1200
+ function fromRRule(text2, start) {
1201
+ const rule = text2.replace(/^RRULE:/i, "").trim();
1202
+ const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
1203
+ const i = p.indexOf("=");
1204
+ return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
1205
+ }));
1206
+ const freq = (kv.get("FREQ") ?? "").toLowerCase();
1207
+ const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
1208
+ if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
1209
+ const out = { freq };
1210
+ const interval = Number(kv.get("INTERVAL") ?? 1);
1211
+ if (!Number.isInteger(interval) || interval < 1) return custom();
1212
+ if (interval > 1) out.interval = interval;
1213
+ if (kv.has("BYDAY")) {
1214
+ const days = kv.get("BYDAY").split(",");
1215
+ if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
1216
+ out.byDay = WEEKDAYS.filter((d) => days.includes(d));
1217
+ }
1218
+ const until = kv.get("UNTIL");
1219
+ if (until) {
1220
+ const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
1221
+ if (!m) return custom();
1222
+ if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
1223
+ else {
1224
+ const at3 = m[7] ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
1225
+ const s = start && start.length > 10 ? asDate(start) : void 0;
1226
+ const minutes = (d) => d.getHours() * 60 + d.getMinutes();
1227
+ if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
1228
+ out.until = format3(at3, "yyyy-MM-dd");
577
1229
  }
1230
+ } else if (kv.has("COUNT")) {
1231
+ const count = Number(kv.get("COUNT"));
1232
+ if (!Number.isInteger(count) || count < 1) return custom();
1233
+ out.count = count;
1234
+ }
1235
+ return out;
1236
+ }
1237
+ function presets(start, locale = "en") {
1238
+ const d = asDate(start);
1239
+ return [
1240
+ { id: "none", label: t(locale, "repeat.none"), recurrence: null },
1241
+ { id: "daily", label: t(locale, "repeat.every.daily"), recurrence: { freq: "daily" } },
1242
+ { id: "weekly", label: t(locale, "repeat.weeklyOn", { day: t(locale, `weekday.on.${weekdayOf(start)}`) }), recurrence: { freq: "weekly" } },
1243
+ { id: "monthly", label: t(locale, "repeat.monthlyOn", { d: d.getDate() }), recurrence: { freq: "monthly" } },
1244
+ { id: "yearly", label: t(locale, "repeat.yearlyOn", { date: fmt(locale, d, "d MMMM") }), recurrence: { freq: "yearly" } },
1245
+ { id: "weekdays", label: t(locale, "repeat.weekdays"), recurrence: { freq: "weekly", byDay: WORKWEEK } }
1246
+ ];
1247
+ }
1248
+ function presetOf(r, start) {
1249
+ if (!r) return "none";
1250
+ if (r.rule) return "custom";
1251
+ const { until: _u, count: _c, ...base } = normalize(r, start);
1252
+ const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
1253
+ return hit?.id ?? "custom";
1254
+ }
1255
+ var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
1256
+ function describe(r, start, locale = "en") {
1257
+ if (!r) return t(locale, "repeat.none");
1258
+ if (r.rule) return t(locale, "repeat.custom", { rule: r.rule });
1259
+ const n = normalize(r, start);
1260
+ const d = asDate(start);
1261
+ const every = n.interval ? t(locale, `repeat.everyN.${n.freq}`, { n: n.interval }) : t(locale, `repeat.every.${n.freq}`);
1262
+ const head = n.freq === "weekly" ? !n.interval && n.byDay?.join() === WORKWEEK.join() ? t(locale, "repeat.weekdays") : t(locale, "repeat.onDays", { every, days: (n.byDay ?? [weekdayOf(start)]).map((x) => shortDay(x, locale)).join(", ") }) : n.freq === "monthly" ? t(locale, "repeat.onDay", { every, d: d.getDate() }) : n.freq === "yearly" ? t(locale, "repeat.onDate", { every, date: fmt(locale, d, "d MMMM") }) : every;
1263
+ const end = n.until ? ` \xB7 ${t(locale, "repeat.until", { date: fmt(locale, parseISO3(n.until), "d MMM yyyy") })}` : n.count ? ` \xB7 ${t(locale, "repeat.count", { n: n.count })}` : "";
1264
+ return head + end;
1265
+ }
1266
+ var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
1267
+ function parseEvery(text2) {
1268
+ const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
1269
+ const n = Number(m?.[1] ?? 1);
1270
+ if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
1271
+ const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
1272
+ return { freq, interval: n > 1 ? n : void 0 };
1273
+ }
1274
+ var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
1275
+ function parseDays(text2) {
1276
+ const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
1277
+ const bad = words.filter((w) => !WEEKDAYS.includes(w));
1278
+ if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
1279
+ return WEEKDAYS.filter((d) => words.includes(d));
1280
+ }
1281
+ var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
1282
+ function parseEnds(text2) {
1283
+ const t2 = text2.trim().toLowerCase();
1284
+ if (!t2 || t2 === "never") return {};
1285
+ if (/^\d{4}-\d{2}-\d{2}$/.test(t2) && isValid(parseISO3(t2)) && format3(parseISO3(t2), "yyyy-MM-dd") === t2) return { until: t2 };
1286
+ const m = /^(\d+)(\s*times?)?$/.exec(t2);
1287
+ if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
1288
+ throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
1289
+ }
1290
+
1291
+ // ../../packages/core/src/providers/google/oauth.ts
1292
+ import { createHash as createHash2, randomBytes } from "crypto";
1293
+ import { createServer } from "http";
1294
+ var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
1295
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
1296
+ var SCOPES = "openid email https://www.googleapis.com/auth/calendar.calendarlist.readonly https://www.googleapis.com/auth/calendar.events";
1297
+ var TIMEOUT_MS = 5 * 60 * 1e3;
1298
+ var clientConfig = {};
1299
+ function setClientConfig(cfg) {
1300
+ clientConfig = cfg;
1301
+ }
1302
+ function getClientConfig() {
1303
+ const { clientId, clientSecret } = clientConfig;
1304
+ if (!clientId || !clientSecret) throw new Error("Google sign-in is not configured in this build: add a CalDAV account, or build from source with MYSTICALS_GOOGLE_CLIENT_ID/SECRET set");
1305
+ return { clientId, clientSecret };
1306
+ }
1307
+ function createPkce() {
1308
+ const verifier = randomBytes(32).toString("base64url");
1309
+ return { verifier, challenge: createHash2("sha256").update(verifier).digest("base64url") };
1310
+ }
1311
+ function buildAuthUrl(p) {
1312
+ const url = new URL(AUTH_URL);
1313
+ url.search = new URLSearchParams({
1314
+ client_id: p.clientId,
1315
+ redirect_uri: p.redirectUri,
1316
+ response_type: "code",
1317
+ scope: SCOPES,
1318
+ state: p.state,
1319
+ code_challenge: p.codeChallenge,
1320
+ code_challenge_method: "S256",
1321
+ access_type: "offline",
1322
+ prompt: "consent"
1323
+ }).toString();
1324
+ return url.toString();
1325
+ }
1326
+ function emailFromIdToken(idToken) {
1327
+ try {
1328
+ const payload = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString("utf8"));
1329
+ return typeof payload.email === "string" ? payload.email : void 0;
1330
+ } catch {
1331
+ return void 0;
1332
+ }
1333
+ }
1334
+ function parseTokenResponse(json, now = Date.now()) {
1335
+ const j = json ?? {};
1336
+ if (typeof j.error === "string") {
1337
+ const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
1338
+ throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
1339
+ }
1340
+ if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
1341
+ const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
1342
+ return {
1343
+ accessToken: j.access_token,
1344
+ expiresAt: now + expiresIn * 1e3,
1345
+ refreshToken: typeof j.refresh_token === "string" ? j.refresh_token : void 0,
1346
+ email: typeof j.id_token === "string" ? emailFromIdToken(j.id_token) : void 0
578
1347
  };
579
- const upsert = (ev, accountId, calendarId) => {
1348
+ }
1349
+ async function postToken(params) {
1350
+ const res = await timedFetch(TOKEN_URL, {
1351
+ method: "POST",
1352
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1353
+ body: new URLSearchParams(params).toString()
1354
+ });
1355
+ const json = await res.json().catch(() => ({ error: `http_${res.status}` }));
1356
+ if (!res.ok && !json.error) json.error = `http_${res.status}`;
1357
+ return parseTokenResponse(json);
1358
+ }
1359
+ var DONE_HTML = (msg, returnUrl) => `<!doctype html><meta charset="utf-8"><title>Mysticals</title><body style="font:15px -apple-system,sans-serif;text-align:center;padding-top:80px"><h2>${msg}</h2>` + (returnUrl ? `<p>Returning to Mysticals\u2026</p><p><a href="${returnUrl}" style="display:inline-block;padding:8px 16px;border-radius:6px;background:#bd93f9;color:#0b0b10;font-weight:600;text-decoration:none">Open Mysticals</a></p><p style="color:#888">You can close this tab.</p><script>location.href=${JSON.stringify(returnUrl)}</script></body>` : `<p>You can close this tab and return to Mysticals.</p></body>`);
1360
+ async function runOAuthFlow(openUrl3, opts = {}) {
1361
+ const cfg = getClientConfig();
1362
+ const { verifier, challenge } = createPkce();
1363
+ const state = randomBytes(16).toString("base64url");
1364
+ const server = createServer();
1365
+ try {
1366
+ await new Promise((resolve, reject) => {
1367
+ server.once("error", reject);
1368
+ server.listen(0, "127.0.0.1", resolve);
1369
+ });
1370
+ const redirectUri = `http://127.0.0.1:${server.address().port}`;
1371
+ const code = await new Promise((resolve, reject) => {
1372
+ const timer = setTimeout(() => reject(new Error("Google sign-in timed out")), TIMEOUT_MS);
1373
+ const finish = (err, value) => {
1374
+ clearTimeout(timer);
1375
+ if (err) reject(err);
1376
+ else resolve(value);
1377
+ };
1378
+ server.on("request", (req, res) => {
1379
+ const params = new URL(req.url ?? "/", redirectUri).searchParams;
1380
+ const reply = (status, msg, then) => {
1381
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
1382
+ res.end(DONE_HTML(msg, status === 200 ? opts.returnUrl : void 0), then);
1383
+ };
1384
+ if (!params.has("code") && !params.has("error")) {
1385
+ res.writeHead(404, { Connection: "close" }).end();
1386
+ return;
1387
+ }
1388
+ if (params.get("state") !== state) {
1389
+ reply(400, "Sign-in failed", () => finish(new Error("Google sign-in failed: state mismatch")));
1390
+ } else if (params.has("error")) {
1391
+ reply(400, "Sign-in cancelled", () => finish(new Error(`Google sign-in failed: ${params.get("error")}`)));
1392
+ } else {
1393
+ const code2 = params.get("code");
1394
+ reply(200, "Signed in", () => finish(null, code2));
1395
+ }
1396
+ });
1397
+ openUrl3(buildAuthUrl({ clientId: cfg.clientId, redirectUri, state, codeChallenge: challenge })).catch(
1398
+ (e) => finish(e instanceof Error ? e : new Error(String(e)))
1399
+ );
1400
+ });
1401
+ opts.onCode?.();
1402
+ const tok = await postToken({
1403
+ grant_type: "authorization_code",
1404
+ code,
1405
+ code_verifier: verifier,
1406
+ redirect_uri: redirectUri,
1407
+ client_id: cfg.clientId,
1408
+ client_secret: cfg.clientSecret
1409
+ });
1410
+ if (!tok.refreshToken) throw new Error("Google did not return a refresh token");
1411
+ const email = tok.email ?? await fetchUserEmail(tok.accessToken);
1412
+ return { email, credentials: { kind: "google", refreshToken: tok.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt } };
1413
+ } finally {
1414
+ server.closeAllConnections();
1415
+ server.close();
1416
+ }
1417
+ }
1418
+ async function fetchUserEmail(accessToken) {
1419
+ const res = await timedFetch("https://openidconnect.googleapis.com/v1/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
1420
+ const json = await res.json().catch(() => ({}));
1421
+ if (!res.ok || typeof json.email !== "string") throw new Error("Could not read Google account email");
1422
+ return json.email;
1423
+ }
1424
+
1425
+ // ../../packages/core/src/providers/google/provider.ts
1426
+ var API = "https://www.googleapis.com/calendar/v3";
1427
+ function conferenceUrlOf(g) {
1428
+ const uri = g.hangoutLink ?? g.conferenceData?.entryPoints?.find((p) => p.entryPointType === "video")?.uri;
1429
+ return uri && /^https?:\/\//i.test(uri) ? uri : void 0;
1430
+ }
1431
+ var STATUSES = ["accepted", "declined", "tentative", "needsAction"];
1432
+ var toStatus = (s) => STATUSES.includes(s) ? s : "needsAction";
1433
+ function mapEvent(g, accountId, calendarId) {
1434
+ const allDay = !!g.start.date;
1435
+ const time = (t2) => allDay ? t2.date : new Date(t2.dateTime).toISOString();
1436
+ const attendees = (g.attendees ?? []).map((a) => ({
1437
+ email: a.email,
1438
+ name: a.displayName,
1439
+ status: toStatus(a.responseStatus),
1440
+ self: a.self || void 0,
1441
+ organizer: a.organizer || void 0
1442
+ }));
1443
+ const self = g.attendees?.find((a) => a.self);
1444
+ return {
1445
+ id: g.id,
1446
+ accountId,
1447
+ calendarId,
1448
+ title: g.summary ?? "",
1449
+ start: time(g.start),
1450
+ end: time(g.end),
1451
+ allDay,
1452
+ location: g.location,
1453
+ conferenceUrl: conferenceUrlOf(g),
1454
+ description: g.description,
1455
+ organizer: g.organizer?.email ? { email: g.organizer.email, name: g.organizer.displayName } : void 0,
1456
+ attendees,
1457
+ myStatus: self ? toStatus(self.responseStatus) : void 0,
1458
+ etag: g.etag,
1459
+ raw: g,
1460
+ recurringEventId: g.recurringEventId
1461
+ };
1462
+ }
1463
+ function truncateRecurrence(recurrence, cutoff) {
1464
+ const until = cutoff.date ? new Date(Date.parse(cutoff.date) - 864e5).toISOString().slice(0, 10).replace(/-/g, "") : new Date(Date.parse(cutoff.dateTime) - 1e3).toISOString().replace(/[-:]|\.\d{3}/g, "");
1465
+ return recurrence.map((line) => {
1466
+ if (!line.startsWith("RRULE:")) return line;
1467
+ const parts = line.slice(6).split(";").filter((p) => !/^(COUNT|UNTIL)=/i.test(p));
1468
+ return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
1469
+ });
1470
+ }
1471
+ var DAY = 864e5;
1472
+ var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY).toISOString().slice(0, 10);
1473
+ var daysBetween = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY);
1474
+ var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
1475
+ function shiftAnchor(anchor, before, start, allDay, timeZone) {
1476
+ if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween(before.date ?? before.dateTime, start)) };
1477
+ if (anchor.dateTime && before.dateTime) {
1478
+ return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
1479
+ }
1480
+ const at3 = new Date(start);
1481
+ const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween(before.date, localDate(at3)))}T00:00:00`);
1482
+ day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
1483
+ return { dateTime: day.toISOString(), timeZone };
1484
+ }
1485
+ function endFrom(from, event, timeZone) {
1486
+ if (event.allDay) return { date: plusDays(from.date, daysBetween(event.start, event.end)) };
1487
+ return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
1488
+ }
1489
+ var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
1490
+ var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
1491
+ var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
1492
+ var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
1493
+ var nulled = (t2) => ({ date: null, dateTime: null, timeZone: null, ...t2 });
1494
+ function createGoogleProviderImpl(ctx) {
1495
+ if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
1496
+ let creds = ctx.credentials;
1497
+ let refreshing;
1498
+ const refresh = () => refreshing ??= (async () => {
1499
+ const { clientId, clientSecret } = getClientConfig();
1500
+ const tok = await postToken({ grant_type: "refresh_token", refresh_token: creds.refreshToken, client_id: clientId, client_secret: clientSecret });
1501
+ creds = { kind: "google", refreshToken: tok.refreshToken ?? creds.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt };
1502
+ await ctx.saveCredentials(creds);
1503
+ return tok.accessToken;
1504
+ })().finally(() => refreshing = void 0);
1505
+ const token = async () => creds.accessToken && (creds.expiresAt ?? 0) > Date.now() + 6e4 ? creds.accessToken : refresh();
1506
+ async function api(method, path, query = {}, body) {
1507
+ const qs = new URLSearchParams(query).toString();
1508
+ const url = `${API}${path}${qs ? `?${qs}` : ""}`;
1509
+ const send = async (accessToken) => timedFetch(url, {
1510
+ method,
1511
+ headers: { Authorization: `Bearer ${accessToken}`, ...body ? { "Content-Type": "application/json" } : {} },
1512
+ body: body ? JSON.stringify(body) : void 0
1513
+ });
1514
+ let res = await send(await token());
1515
+ if (res.status === 401) res = await send(await refresh());
1516
+ if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
1517
+ if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
1518
+ if (!res.ok) {
1519
+ const err = await res.json().catch(() => ({}));
1520
+ throw new Error(`Google API ${res.status}: ${err.error?.message ?? res.statusText}`);
1521
+ }
1522
+ return await res.json();
1523
+ }
1524
+ const eventPath = (calendarId, eventId) => `/calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`;
1525
+ const rawOf = (e) => e.raw;
1526
+ const isOrganizer = (e) => {
1527
+ const org = rawOf(e)?.organizer;
1528
+ if (org?.self !== void 0) return org.self;
1529
+ return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
1530
+ };
1531
+ const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
1532
+ const fields = (event, prevAttendees, organizer) => {
1533
+ const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
1534
+ if (organizer) {
1535
+ const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
1536
+ body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
1537
+ }
1538
+ return body;
1539
+ };
1540
+ async function remainingRecurrence(calendarId, series, recurrence, slot) {
1541
+ const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
1542
+ if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
1543
+ const cut = Date.parse(slot.dateTime ?? slot.date);
1544
+ let passed = 0;
1545
+ let pageToken;
1546
+ do {
1547
+ const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
1548
+ showDeleted: "true",
1549
+ timeMax: new Date(cut).toISOString(),
1550
+ maxResults: "2500",
1551
+ ...pageToken ? { pageToken } : {}
1552
+ });
1553
+ for (const g of page.items ?? []) {
1554
+ const t2 = g.originalStartTime ?? g.start;
1555
+ if (Date.parse(t2.dateTime ?? t2.date) < cut) passed++;
1556
+ }
1557
+ pageToken = page.nextPageToken;
1558
+ } while (pageToken);
1559
+ return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
1560
+ }
1561
+ return {
1562
+ async listCalendars() {
1563
+ const out = [];
1564
+ let pageToken;
1565
+ do {
1566
+ const page = await api(
1567
+ "GET",
1568
+ "/users/me/calendarList",
1569
+ pageToken ? { pageToken } : {}
1570
+ );
1571
+ for (const c of page.items ?? [])
1572
+ out.push({
1573
+ id: c.id,
1574
+ accountId: ctx.accountId,
1575
+ name: c.summaryOverride ?? c.summary ?? c.id,
1576
+ color: c.backgroundColor ?? "#4285f4",
1577
+ readOnly: c.accessRole !== "owner" && c.accessRole !== "writer"
1578
+ });
1579
+ pageToken = page.nextPageToken;
1580
+ } while (pageToken);
1581
+ return out;
1582
+ },
1583
+ async listEvents(calendarId, range) {
1584
+ const out = [];
1585
+ let pageToken;
1586
+ do {
1587
+ const page = await api("GET", eventPath(calendarId), {
1588
+ singleEvents: "true",
1589
+ timeMin: new Date(range.start).toISOString(),
1590
+ timeMax: new Date(range.end).toISOString(),
1591
+ maxResults: "2500",
1592
+ ...pageToken ? { pageToken } : {}
1593
+ });
1594
+ for (const g of page.items ?? []) if (g.status !== "cancelled") out.push(map(g, calendarId));
1595
+ pageToken = page.nextPageToken;
1596
+ } while (pageToken);
1597
+ return out;
1598
+ },
1599
+ async createEvent(calendarId, input) {
1600
+ const attendees = (input.attendees ?? []).map((email) => ({ email }));
1601
+ const rule = input.recurrence;
1602
+ const g = await api(
1603
+ "POST",
1604
+ eventPath(calendarId),
1605
+ { sendUpdates: attendees.length ? "all" : "none" },
1606
+ {
1607
+ summary: input.title,
1608
+ start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
1609
+ end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
1610
+ location: input.location,
1611
+ description: input.description,
1612
+ attendees,
1613
+ ...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
1614
+ }
1615
+ );
1616
+ return map(g, calendarId);
1617
+ },
1618
+ async updateEvent(event, scope = "one") {
1619
+ const organizer = isOrganizer(event);
1620
+ const q = { sendUpdates: organizer ? "all" : "none" };
1621
+ const series = event.recurringEventId;
1622
+ const before = rawOf(event)?.start;
1623
+ if (!series || scope === "one" || !before) {
1624
+ const rule2 = !series ? event.recurrence : void 0;
1625
+ const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
1626
+ const body = {
1627
+ ...fields(event, rawOf(event)?.attendees, organizer),
1628
+ start: time(event.start),
1629
+ end: time(event.end),
1630
+ ...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
1631
+ };
1632
+ return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
1633
+ }
1634
+ const master = await api("GET", eventPath(event.calendarId, series));
1635
+ const timeZone = master.start.timeZone ?? localZone();
1636
+ const rule = event.recurrence;
1637
+ const slot = rawOf(event)?.originalStartTime ?? before;
1638
+ const first = master.start.dateTime ?? master.start.date;
1639
+ const at3 = slot.dateTime ?? slot.date;
1640
+ if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
1641
+ const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
1642
+ const body = {
1643
+ ...fields(event, master.attendees, organizer),
1644
+ start: nulled(start2),
1645
+ end: nulled(endFrom(start2, event, timeZone)),
1646
+ // A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
1647
+ ...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
1648
+ };
1649
+ await api("PATCH", eventPath(event.calendarId, series), q, body);
1650
+ return event;
1651
+ }
1652
+ const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
1653
+ await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
1654
+ const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
1655
+ const created = await api("POST", eventPath(event.calendarId), q, {
1656
+ ...fields(event, master.attendees, organizer),
1657
+ start,
1658
+ end: endFrom(start, event, timeZone),
1659
+ ...recurrence ? { recurrence } : {}
1660
+ });
1661
+ return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
1662
+ },
1663
+ async deleteEvent(event, scope = "one") {
1664
+ const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
1665
+ const series = event.recurringEventId;
1666
+ if (!series || scope === "one") return void await api("DELETE", eventPath(event.calendarId, event.id), q);
1667
+ const cutoff = rawOf(event)?.originalStartTime;
1668
+ if (scope === "following" && cutoff) {
1669
+ const master = await api("GET", eventPath(event.calendarId, series));
1670
+ const first = master.start.dateTime ?? master.start.date;
1671
+ const at3 = cutoff.dateTime ?? cutoff.date;
1672
+ if (Date.parse(at3) > Date.parse(first)) {
1673
+ const recurrence = truncateRecurrence(master.recurrence ?? [], cutoff);
1674
+ return void await api("PATCH", eventPath(event.calendarId, series), q, { recurrence });
1675
+ }
1676
+ }
1677
+ await api("DELETE", eventPath(event.calendarId, series), q);
1678
+ },
1679
+ async getRecurrence(event) {
1680
+ if (!event.recurringEventId) return null;
1681
+ const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
1682
+ const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
1683
+ return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
1684
+ },
1685
+ async respond(event, status) {
1686
+ const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
1687
+ if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
1688
+ const g = await api(
1689
+ "PATCH",
1690
+ eventPath(event.calendarId, event.id),
1691
+ { sendUpdates: "all" },
1692
+ { attendees: attendees.map((a) => a.self ? { ...a, responseStatus: status } : a) }
1693
+ );
1694
+ return map(g, event.calendarId);
1695
+ }
1696
+ };
1697
+ }
1698
+
1699
+ // ../../packages/core/src/api.ts
1700
+ var PALETTE = ["#bd93f9", "#50fa7b", "#8be9fd", "#ff79c6", "#ffb86c", "#f1fa8c"];
1701
+ var id = z.string().min(1).max(1024);
1702
+ var isoDateOrTime = z.union([z.iso.datetime({ offset: true }), z.iso.date()]);
1703
+ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
1704
+ var text = (max2) => z.string().max(max2);
1705
+ var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
1706
+ var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
1707
+ var Recurrence = z.object({
1708
+ freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
1709
+ interval: z.number().int().min(1).max(999).optional(),
1710
+ byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
1711
+ until: z.iso.date().optional(),
1712
+ count: z.number().int().min(1).max(999).optional(),
1713
+ // An RRULE value kept from the provider: one line of plain RRULE parts only.
1714
+ rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
1715
+ }).strict();
1716
+ var NewEvent = z.object({
1717
+ accountId: id,
1718
+ calendarId: id,
1719
+ title: text(1e3),
1720
+ start: isoDateOrTime,
1721
+ end: isoDateOrTime,
1722
+ allDay: z.boolean(),
1723
+ location: text(1e3).optional(),
1724
+ description: text(2e4).optional(),
1725
+ attendees: z.array(z.email()).max(500).optional(),
1726
+ recurrence: Recurrence.optional()
1727
+ }).refine(ordered, "end before start");
1728
+ var Attendee = z.object({
1729
+ email: text(320).min(1),
1730
+ name: text(500).optional(),
1731
+ status: z.enum(["accepted", "declined", "tentative", "needsAction"]),
1732
+ self: z.boolean().optional(),
1733
+ organizer: z.boolean().optional()
1734
+ });
1735
+ var EventRef = z.object({ id, accountId: id, calendarId: id });
1736
+ var EventEdit = EventRef.extend({
1737
+ title: text(1e3),
1738
+ start: isoDateOrTime,
1739
+ end: isoDateOrTime,
1740
+ allDay: z.boolean(),
1741
+ location: text(1e3).optional(),
1742
+ description: text(2e4).optional(),
1743
+ attendees: z.array(Attendee).max(500),
1744
+ // Absent keeps the series' rule; null stops repeating.
1745
+ recurrence: Recurrence.nullish()
1746
+ }).refine(ordered, "end before start");
1747
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
1748
+ var CaldavInput = z.object({
1749
+ label: text(200).min(1),
1750
+ serverUrl: z.string().refine((u) => {
1751
+ if (!URL.canParse(u)) return false;
1752
+ const { protocol, hostname } = new URL(u);
1753
+ return protocol === "https:" || protocol === "http:" && LOOPBACK.has(hostname);
1754
+ }, "server URL must be https"),
1755
+ username: text(500).min(1),
1756
+ password: text(2e3).min(1),
1757
+ color: color.optional()
1758
+ });
1759
+ var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
1760
+ var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
1761
+ var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
1762
+ var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
1763
+ function createApi(store, sync, deps) {
1764
+ const account = (accountId) => {
1765
+ const a = store.get(id.parse(accountId));
1766
+ if (!a) throw new Error("unknown account");
1767
+ return a;
1768
+ };
1769
+ const syncOne = (accountId) => {
1770
+ sync.syncNow(accountId, { quiet: true, fresh: true }).catch((e) => console.error(`sync ${accountId} failed`, e));
1771
+ };
1772
+ const applyLocal = (accountId, fn) => {
1773
+ try {
1774
+ store.patchCache(accountId, (c) => ({ ...c, events: fn(c.events) }));
1775
+ sync.edited(accountId);
1776
+ deps.onChanged?.(accountId);
1777
+ } catch (e) {
1778
+ console.error(`local update of ${accountId} failed`, e);
1779
+ }
1780
+ };
1781
+ const upsert = (ev, accountId, calendarId) => {
580
1782
  const own = { ...ev, accountId, calendarId };
581
1783
  applyLocal(accountId, (events) => [...events.filter((e) => e.id !== own.id), own]);
582
1784
  };
@@ -661,7 +1863,10 @@ function createApi(store, sync, deps) {
661
1863
  },
662
1864
  events: {
663
1865
  // `raw` stays in the daemon/main process: it can be huge (CalDAV series ICS) and writes re-read it from the cache.
664
- list: async (raw) => queryEvents(store, Range.parse(raw), false).map(({ raw: _raw, ...e }) => e),
1866
+ // Caches synced before `conferenceUrl` existed still hold the Meet link in the Google resource.
1867
+ list: async (raw) => queryEvents(store, Range.parse(raw), false).map(
1868
+ ({ raw: r, ...e }) => e.conferenceUrl || !r || typeof r !== "object" ? e : { ...e, conferenceUrl: conferenceUrlOf(r) }
1869
+ ),
665
1870
  create: async (raw) => {
666
1871
  const input = NewEvent.parse(raw);
667
1872
  const a = account(input.accountId);
@@ -866,184 +2071,51 @@ var AccountStore = class {
866
2071
  const hit = this.caches.get(id2);
867
2072
  if (hit) return hit;
868
2073
  const cache = readJson(join4(this.accountDir(id2), "cache.json"), { calendars: [], events: [] });
869
- if (this.get(id2)) this.caches.set(id2, cache);
870
- return cache;
871
- }
872
- writeCache(id2, cache) {
873
- return this.serial(async () => {
874
- const dir = this.accountDir(id2);
875
- this.require(id2);
876
- await mkdir(dir, { recursive: true });
877
- await writeAtomic(join4(dir, "cache.json"), JSON.stringify(cache));
878
- this.caches.set(id2, cache);
879
- });
880
- }
881
- /** In-memory only: shows a local edit at once; the next sync writes the server's copy to disk. */
882
- patchCache(id2, fn) {
883
- this.require(id2);
884
- this.caches.set(id2, fn(this.readCache(id2)));
885
- }
886
- hiddenCalendars(id2) {
887
- const file = join4(this.accountDir(id2), "prefs.json");
888
- try {
889
- const hidden = readJson(file, {}).hidden;
890
- return Array.isArray(hidden) ? hidden.filter((h) => typeof h === "string") : [];
891
- } catch {
892
- return [];
893
- }
894
- }
895
- setCalendarVisible(id2, calendarId, visible) {
896
- return this.serial(async () => {
897
- this.require(id2);
898
- const hidden = new Set(this.hiddenCalendars(id2));
899
- if (visible) hidden.delete(calendarId);
900
- else hidden.add(calendarId);
901
- const dir = this.accountDir(id2);
902
- await mkdir(dir, { recursive: true });
903
- await writeAtomic(join4(dir, "prefs.json"), JSON.stringify({ hidden: [...hidden] }));
904
- });
905
- }
906
- };
907
-
908
- // ../../packages/core/src/mock/mockApi.ts
909
- import { addDays as addDays2, format as format4 } from "date-fns";
910
-
911
- // ../../packages/core/src/mock/MockProvider.ts
912
- import { randomUUID as randomUUID3 } from "crypto";
913
- import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format3, parseISO as parseISO4, startOfWeek } from "date-fns";
914
-
915
- // ../../packages/core/src/logic/recurrence.ts
916
- import { format as format2, isValid, parseISO as parseISO3 } from "date-fns";
917
- var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
918
- var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
919
- var SHORT = { MO: "Mon", TU: "Tue", WE: "Wed", TH: "Thu", FR: "Fri", SA: "Sat", SU: "Sun" };
920
- var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
921
- var FREQS = ["daily", "weekly", "monthly", "yearly"];
922
- var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
923
- var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
924
- var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
925
- var shortDay = (d) => SHORT[d];
926
- function normalize(r, start) {
927
- if (r.rule) return { freq: r.freq, rule: r.rule };
928
- const out = { freq: r.freq };
929
- if (r.interval && r.interval > 1) out.interval = r.interval;
930
- if (r.freq === "weekly" && r.byDay?.length) {
931
- const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
932
- if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
933
- }
934
- if (r.until) out.until = r.until;
935
- else if (r.count) out.count = r.count;
936
- return out;
937
- }
938
- function sameRecurrence(a, b, start) {
939
- if (!a || !b) return a === b;
940
- return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
941
- }
942
- function toRRule(r, allDay) {
943
- if (r.rule) return r.rule;
944
- const parts = [`FREQ=${r.freq.toUpperCase()}`];
945
- if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
946
- if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
947
- if (r.until) {
948
- const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
949
- parts.push(`UNTIL=${until}`);
950
- } else if (r.count) parts.push(`COUNT=${r.count}`);
951
- return parts.join(";");
952
- }
953
- var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
954
- function fromRRule(text2, start) {
955
- const rule = text2.replace(/^RRULE:/i, "").trim();
956
- const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
957
- const i = p.indexOf("=");
958
- return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
959
- }));
960
- const freq = (kv.get("FREQ") ?? "").toLowerCase();
961
- const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
962
- if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
963
- const out = { freq };
964
- const interval = Number(kv.get("INTERVAL") ?? 1);
965
- if (!Number.isInteger(interval) || interval < 1) return custom();
966
- if (interval > 1) out.interval = interval;
967
- if (kv.has("BYDAY")) {
968
- const days = kv.get("BYDAY").split(",");
969
- if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
970
- out.byDay = WEEKDAYS.filter((d) => days.includes(d));
971
- }
972
- const until = kv.get("UNTIL");
973
- if (until) {
974
- const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
975
- if (!m) return custom();
976
- if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
977
- else {
978
- const at3 = m[7] ? new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])) : new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
979
- const s = start && start.length > 10 ? asDate(start) : void 0;
980
- const minutes = (d) => d.getHours() * 60 + d.getMinutes();
981
- if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
982
- out.until = format2(at3, "yyyy-MM-dd");
983
- }
984
- } else if (kv.has("COUNT")) {
985
- const count = Number(kv.get("COUNT"));
986
- if (!Number.isInteger(count) || count < 1) return custom();
987
- out.count = count;
988
- }
989
- return out;
990
- }
991
- function presets(start) {
992
- const d = asDate(start);
993
- return [
994
- { id: "none", label: "Does not repeat", recurrence: null },
995
- { id: "daily", label: "Every day", recurrence: { freq: "daily" } },
996
- { id: "weekly", label: `Every week on ${format2(d, "EEEE")}`, recurrence: { freq: "weekly" } },
997
- { id: "monthly", label: `Every month on day ${d.getDate()}`, recurrence: { freq: "monthly" } },
998
- { id: "yearly", label: `Every year on ${format2(d, "d MMMM")}`, recurrence: { freq: "yearly" } },
999
- { id: "weekdays", label: "Every weekday (Mon\u2013Fri)", recurrence: { freq: "weekly", byDay: WORKWEEK } }
1000
- ];
1001
- }
1002
- function presetOf(r, start) {
1003
- if (!r) return "none";
1004
- if (r.rule) return "custom";
1005
- const { until: _u, count: _c, ...base } = normalize(r, start);
1006
- const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
1007
- return hit?.id ?? "custom";
1008
- }
1009
- var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
1010
- function describe(r, start) {
1011
- if (!r) return "Does not repeat";
1012
- if (r.rule) return `Custom rule (${r.rule})`;
1013
- const n = normalize(r, start);
1014
- const d = asDate(start);
1015
- const every = n.interval ? `Every ${n.interval} ${UNIT[n.freq]}s` : `Every ${UNIT[n.freq]}`;
1016
- const on = n.freq === "weekly" ? !n.interval && n.byDay?.join() === WORKWEEK.join() ? "" : ` on ${(n.byDay ?? [weekdayOf(start)]).map(shortDay).join(", ")}` : n.freq === "monthly" ? ` on day ${d.getDate()}` : n.freq === "yearly" ? ` on ${format2(d, "d MMMM")}` : "";
1017
- const head = n.freq === "weekly" && !n.interval && n.byDay?.join() === WORKWEEK.join() ? "Every weekday (Mon\u2013Fri)" : `${every}${on}`;
1018
- const end = n.until ? ` \xB7 until ${format2(parseISO3(n.until), "d MMM yyyy")}` : n.count ? ` \xB7 ${n.count} time${n.count > 1 ? "s" : ""}` : "";
1019
- return head + end;
1020
- }
1021
- var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
1022
- function parseEvery(text2) {
1023
- const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
1024
- const n = Number(m?.[1] ?? 1);
1025
- if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
1026
- const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
1027
- return { freq, interval: n > 1 ? n : void 0 };
1028
- }
1029
- var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
1030
- function parseDays(text2) {
1031
- const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
1032
- const bad = words.filter((w) => !WEEKDAYS.includes(w));
1033
- if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
1034
- return WEEKDAYS.filter((d) => words.includes(d));
1035
- }
1036
- var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
1037
- function parseEnds(text2) {
1038
- const t = text2.trim().toLowerCase();
1039
- if (!t || t === "never") return {};
1040
- if (/^\d{4}-\d{2}-\d{2}$/.test(t) && isValid(parseISO3(t)) && format2(parseISO3(t), "yyyy-MM-dd") === t) return { until: t };
1041
- const m = /^(\d+)(\s*times?)?$/.exec(t);
1042
- if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
1043
- throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
1044
- }
2074
+ if (this.get(id2)) this.caches.set(id2, cache);
2075
+ return cache;
2076
+ }
2077
+ writeCache(id2, cache) {
2078
+ return this.serial(async () => {
2079
+ const dir = this.accountDir(id2);
2080
+ this.require(id2);
2081
+ await mkdir(dir, { recursive: true });
2082
+ await writeAtomic(join4(dir, "cache.json"), JSON.stringify(cache));
2083
+ this.caches.set(id2, cache);
2084
+ });
2085
+ }
2086
+ /** In-memory only: shows a local edit at once; the next sync writes the server's copy to disk. */
2087
+ patchCache(id2, fn) {
2088
+ this.require(id2);
2089
+ this.caches.set(id2, fn(this.readCache(id2)));
2090
+ }
2091
+ hiddenCalendars(id2) {
2092
+ const file = join4(this.accountDir(id2), "prefs.json");
2093
+ try {
2094
+ const hidden = readJson(file, {}).hidden;
2095
+ return Array.isArray(hidden) ? hidden.filter((h) => typeof h === "string") : [];
2096
+ } catch {
2097
+ return [];
2098
+ }
2099
+ }
2100
+ setCalendarVisible(id2, calendarId, visible) {
2101
+ return this.serial(async () => {
2102
+ this.require(id2);
2103
+ const hidden = new Set(this.hiddenCalendars(id2));
2104
+ if (visible) hidden.delete(calendarId);
2105
+ else hidden.add(calendarId);
2106
+ const dir = this.accountDir(id2);
2107
+ await mkdir(dir, { recursive: true });
2108
+ await writeAtomic(join4(dir, "prefs.json"), JSON.stringify({ hidden: [...hidden] }));
2109
+ });
2110
+ }
2111
+ };
2112
+
2113
+ // ../../packages/core/src/mock/mockApi.ts
2114
+ import { addDays as addDays2, format as format5 } from "date-fns";
1045
2115
 
1046
2116
  // ../../packages/core/src/mock/MockProvider.ts
2117
+ import { randomUUID as randomUUID3 } from "crypto";
2118
+ import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format4, parseISO as parseISO4, startOfWeek } from "date-fns";
1047
2119
  var MAX_INSTANCES = 60;
1048
2120
  function expand(first, r, series) {
1049
2121
  const start = parseISO4(first.start);
@@ -1055,7 +2127,7 @@ function expand(first, r, series) {
1055
2127
  const n = differenceInCalendarDays(d, start);
1056
2128
  if (r.freq === "daily") return n % every === 0;
1057
2129
  if (r.freq === "weekly")
1058
- return days.includes(weekdayOf(format3(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
2130
+ return days.includes(weekdayOf(format4(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
1059
2131
  const months = differenceInCalendarMonths(d, start);
1060
2132
  if (d.getDate() !== start.getDate()) return false;
1061
2133
  return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
@@ -1065,8 +2137,8 @@ function expand(first, r, series) {
1065
2137
  const d = addDays(start, n);
1066
2138
  if (until && differenceInCalendarDays(d, until) > 0) break;
1067
2139
  if (!hits(d)) continue;
1068
- const s = first.allDay ? format3(d, "yyyy-MM-dd") : d.toISOString();
1069
- const e = first.allDay ? format3(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
2140
+ const s = first.allDay ? format4(d, "yyyy-MM-dd") : d.toISOString();
2141
+ const e = first.allDay ? format4(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
1070
2142
  out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
1071
2143
  }
1072
2144
  return out;
@@ -1087,9 +2159,9 @@ var MockProvider = class {
1087
2159
  return structuredClone(this.calendars);
1088
2160
  }
1089
2161
  async listEvents(calendarId, range) {
1090
- const t = (iso2) => parseISO4(iso2).getTime();
2162
+ const t2 = (iso2) => parseISO4(iso2).getTime();
1091
2163
  return structuredClone(
1092
- this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
2164
+ this.events.filter((e) => e.calendarId === calendarId && t2(e.end) > t2(range.start) && t2(e.start) < t2(range.end))
1093
2165
  );
1094
2166
  }
1095
2167
  async createEvent(calendarId, input) {
@@ -1229,8 +2301,11 @@ function createMockApi(onChanged) {
1229
2301
  seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
1230
2302
  }
1231
2303
  personal.rules.set("run-series", { freq: "daily", count: 7 });
2304
+ seed(personal, "p-main", { title: "Ukraine - Portugal", start: iso(5, 12, 30), end: iso(5, 13), conferenceUrl: "https://meet.google.com/zon-fdwf-hnk" });
2305
+ seed(personal, "p-main", { title: "Catch-up", start: iso(5, 16), end: iso(5, 16, 30), location: "https://meet.namechip.net/catch-up" });
2306
+ seed(personal, "p-main", { title: "Coffee", start: iso(5, 9), end: iso(5, 9, 30), location: "Podil https://maps.app.goo.gl/kyiv-coffee" });
1232
2307
  seed(personal, "p-main", { title: "Dinner with friends", start: iso(2, 20), end: iso(2, 22), location: "Kyiv" });
1233
- const day = (offset) => format4(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
2308
+ const day = (offset) => format5(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
1234
2309
  seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
1235
2310
  for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
1236
2311
  seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
@@ -1356,16 +2431,16 @@ function parse(ics) {
1356
2431
  }
1357
2432
  return root;
1358
2433
  }
1359
- var toIso = (t) => t.isDate ? t.toString() : t.toJSDate().toISOString();
2434
+ var toIso = (t2) => t2.isDate ? t2.toString() : t2.toJSDate().toISOString();
1360
2435
  function fromIso(iso2, allDay) {
1361
2436
  if (allDay) return ICAL.Time.fromDateString(iso2.slice(0, 10));
1362
2437
  const d = new Date(iso2);
1363
2438
  if (Number.isNaN(d.getTime())) throw new Error(`Invalid date: ${iso2}`);
1364
2439
  return ICAL.Time.fromJSDate(d, true);
1365
2440
  }
1366
- function setTime(vevent, name, t) {
2441
+ function setTime(vevent, name, t2) {
1367
2442
  vevent.removeAllProperties(name);
1368
- vevent.addPropertyWithValue(name, t);
2443
+ vevent.addPropertyWithValue(name, t2);
1369
2444
  }
1370
2445
  function setText(vevent, name, value) {
1371
2446
  vevent.removeAllProperties(name);
@@ -1477,15 +2552,15 @@ function applyRule(root, master, event) {
1477
2552
  return;
1478
2553
  }
1479
2554
  setRule(master, event.recurrence, event.allDay);
1480
- const ms = (t) => t.toJSDate().getTime();
2555
+ const ms = (t2) => t2.toJSDate().getTime();
1481
2556
  const rid = (o) => o.getFirstPropertyValue("recurrence-id");
1482
- const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
2557
+ const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t2) => t2 instanceof ICAL.Time);
1483
2558
  const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
1484
2559
  const slots2 = /* @__PURE__ */ new Set();
1485
2560
  const it = master.getFirstPropertyValue("rrule").iterator(dtstart(master));
1486
- for (let t = it.next(), n = 0; t && ms(t) <= last && n < MAX_INSTANCES2; t = it.next(), n++) slots2.add(ms(t));
2561
+ for (let t2 = it.next(), n = 0; t2 && ms(t2) <= last && n < MAX_INSTANCES2; t2 = it.next(), n++) slots2.add(ms(t2));
1487
2562
  for (const o of overrides) if (!slots2.has(ms(rid(o)))) root.removeSubcomponent(o);
1488
- mapDates(master, "exdate", (t) => slots2.has(ms(t)) ? t : null);
2563
+ mapDates(master, "exdate", (t2) => slots2.has(ms(t2)) ? t2 : null);
1489
2564
  }
1490
2565
  function setAttendees(v, emails, email) {
1491
2566
  const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
@@ -1520,8 +2595,8 @@ function findOverride(root, rid) {
1520
2595
  const master = findMaster(root);
1521
2596
  const want = master ? ridTime(master, rid).time.toJSDate().getTime() : NaN;
1522
2597
  return root.getAllSubcomponents("vevent").find((v) => {
1523
- const t = v.getFirstPropertyValue("recurrence-id");
1524
- return !!t && (String(t) === rid || t.toJSDate().getTime() === want);
2598
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2599
+ return !!t2 && (String(t2) === rid || t2.toJSDate().getTime() === want);
1525
2600
  });
1526
2601
  }
1527
2602
  function ridProp(master, name, rid) {
@@ -1568,27 +2643,27 @@ function bump(v) {
1568
2643
  v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
1569
2644
  touch(v);
1570
2645
  }
1571
- function timeProp(name, t) {
2646
+ function timeProp(name, t2) {
1572
2647
  const p = new ICAL.Property(name);
1573
- p.setValue(t);
1574
- const tzid = t.zone?.tzid;
1575
- if (!t.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
2648
+ p.setValue(t2);
2649
+ const tzid = t2.zone?.tzid;
2650
+ if (!t2.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
1576
2651
  return p;
1577
2652
  }
1578
- function replaceTime(v, name, t) {
2653
+ function replaceTime(v, name, t2) {
1579
2654
  v.removeAllProperties(name);
1580
- return timeProp(name, t);
2655
+ return timeProp(name, t2);
1581
2656
  }
1582
2657
  var dtstart = (v) => v.getFirstPropertyValue("dtstart");
1583
- var dateOf = (t) => ICAL.Time.fromData({ year: t.year, month: t.month, day: t.day, isDate: true });
1584
- var daysBetween = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
1585
- function addDays3(t, days) {
1586
- const x = t.clone();
2658
+ var dateOf = (t2) => ICAL.Time.fromData({ year: t2.year, month: t2.month, day: t2.day, isDate: true });
2659
+ var daysBetween2 = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
2660
+ function addDays3(t2, days) {
2661
+ const x = t2.clone();
1587
2662
  x.adjust(days, 0, 0, 0);
1588
2663
  return x;
1589
2664
  }
1590
2665
  function spanFrom(start, event) {
1591
- if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
2666
+ if (event.allDay) return [start, addDays3(start, daysBetween2(fromIso(event.start, true), fromIso(event.end, true)))];
1592
2667
  const end = start.clone();
1593
2668
  end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
1594
2669
  return [start, end];
@@ -1596,23 +2671,23 @@ function spanFrom(start, event) {
1596
2671
  function slotShift(master, before, event) {
1597
2672
  const anchor = dtstart(master);
1598
2673
  const zone = anchor.isDate ? void 0 : anchor.zone;
1599
- const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
2674
+ const local = (t2) => zone && !t2.isDate ? t2.convertToZone(zone) : t2.clone();
1600
2675
  const after = fromIso(event.start, event.allDay);
1601
2676
  if (event.allDay) {
1602
- const days2 = daysBetween(local(before), after);
1603
- return (t) => addDays3(dateOf(local(t)), days2);
2677
+ const days2 = daysBetween2(local(before), after);
2678
+ return (t2) => addDays3(dateOf(local(t2)), days2);
1604
2679
  }
1605
2680
  if (!anchor.isDate) {
1606
2681
  const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
1607
- return (t) => {
1608
- const x = local(t);
2682
+ return (t2) => {
2683
+ const x = local(t2);
1609
2684
  x.adjust(0, 0, 0, seconds);
1610
2685
  return x;
1611
2686
  };
1612
2687
  }
1613
- const days = daysBetween(before, after);
1614
- return (t) => {
1615
- const d = addDays3(dateOf(t), days);
2688
+ const days = daysBetween2(before, after);
2689
+ return (t2) => {
2690
+ const d = addDays3(dateOf(t2), days);
1616
2691
  return ICAL.Time.fromData({ year: d.year, month: d.month, day: d.day, hour: after.hour, minute: after.minute, second: after.second }, ICAL.Timezone.utcTimezone);
1617
2692
  };
1618
2693
  }
@@ -1628,8 +2703,8 @@ function mapDates(v, name, map) {
1628
2703
  out.push(keep);
1629
2704
  continue;
1630
2705
  }
1631
- const t = map(value);
1632
- if (t) out.push(timeProp(name, t));
2706
+ const t2 = map(value);
2707
+ if (t2) out.push(timeProp(name, t2));
1633
2708
  }
1634
2709
  }
1635
2710
  v.removeAllProperties(name);
@@ -1687,10 +2762,10 @@ function applySplitFollowing(event, email, uid) {
1687
2762
  const edited = findOverride(root, raw.recurrenceId);
1688
2763
  const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1689
2764
  for (const v of root.getAllSubcomponents("vevent")) {
1690
- const t = v.getFirstPropertyValue("recurrence-id");
1691
- if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
2765
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2766
+ if (t2 && t2.toJSDate().getTime() < cut) root.removeSubcomponent(v);
1692
2767
  }
1693
- const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
2768
+ const later = (t2) => t2.toJSDate().getTime() >= cut ? t2 : null;
1694
2769
  mapDates(master, "exdate", later);
1695
2770
  mapDates(master, "rdate", later);
1696
2771
  const first = dtstart(master);
@@ -1699,7 +2774,7 @@ function applySplitFollowing(event, email, uid) {
1699
2774
  if (!r.count) continue;
1700
2775
  const it = r.iterator(first);
1701
2776
  let passed = 0;
1702
- for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
2777
+ for (let t2 = it.next(); t2 && t2.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t2 = it.next()) passed++;
1703
2778
  r.count = Math.max(1, r.count - passed);
1704
2779
  p.setValue(r);
1705
2780
  }
@@ -1733,8 +2808,8 @@ function applyDeleteFollowing(raw) {
1733
2808
  const cut = time.toJSDate().getTime();
1734
2809
  if (cut <= master.getFirstPropertyValue("dtstart").toJSDate().getTime()) return null;
1735
2810
  for (const v of root.getAllSubcomponents("vevent")) {
1736
- const t = v.getFirstPropertyValue("recurrence-id");
1737
- if (t && t.toJSDate().getTime() >= cut) root.removeSubcomponent(v);
2811
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2812
+ if (t2 && t2.toJSDate().getTime() >= cut) root.removeSubcomponent(v);
1738
2813
  }
1739
2814
  let until = time.clone();
1740
2815
  if (until.isDate) until.adjust(-1, 0, 0, 0);
@@ -1745,589 +2820,186 @@ function applyDeleteFollowing(raw) {
1745
2820
  for (const p of master.getAllProperties("rrule")) {
1746
2821
  const r = p.getFirstValue().clone();
1747
2822
  r.count = null;
1748
- r.until = until;
1749
- p.setValue(r);
1750
- }
1751
- master.updatePropertyWithValue("sequence", Number(master.getFirstPropertyValue("sequence") ?? 0) + 1);
1752
- touch(master);
1753
- return root.toString();
1754
- }
1755
- function applyRespond(raw, email, status) {
1756
- const root = parse(raw.ics);
1757
- let found = false;
1758
- for (const v of root.getAllSubcomponents("vevent")) {
1759
- for (const p of v.getAllProperties("attendee")) {
1760
- if (!sameEmail(p.getFirstValue(), email)) continue;
1761
- p.setParameter("partstat", PARTSTAT_OUT[status]);
1762
- p.removeParameter("rsvp");
1763
- p.removeParameter("schedule-status");
1764
- found = true;
1765
- }
1766
- if (found) v.updatePropertyWithValue("dtstamp", ICAL.Time.now());
1767
- }
1768
- if (!found) throw new Error("This account is not an attendee of the event");
1769
- return root.toString();
1770
- }
1771
-
1772
- // ../../packages/core/src/providers/caldav/index.ts
1773
- var FALLBACK_COLOR = "#8e8e93";
1774
- var DAY = 864e5;
1775
- async function connect2(serverUrl, username, password) {
1776
- try {
1777
- new URL(serverUrl);
1778
- } catch {
1779
- throw new Error(`Invalid server URL: ${serverUrl}`);
1780
- }
1781
- const headers = getBasicAuthHeaders({ username, password });
1782
- let unauthorized = false;
1783
- const trackingFetch = async (input, init) => {
1784
- const res = await timedFetch(input, init);
1785
- if (res.status === 401) unauthorized = true;
1786
- return res;
1787
- };
1788
- try {
1789
- const account = await createAccount({
1790
- account: { serverUrl, accountType: "caldav", credentials: { username, password } },
1791
- headers,
1792
- fetch: trackingFetch
1793
- });
1794
- return { account, headers };
1795
- } catch (e) {
1796
- if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
1797
- throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
1798
- }
1799
- }
1800
- function toCalendar(c, accountId) {
1801
- const name = typeof c.displayName === "string" && c.displayName ? c.displayName : decodeURIComponent(c.url.split("/").filter(Boolean).pop() ?? c.url);
1802
- const color2 = typeof c.calendarColor === "string" && /^#[0-9a-f]{6}/i.test(c.calendarColor) ? c.calendarColor.slice(0, 7) : FALLBACK_COLOR;
1803
- const privileges = JSON.stringify(c.projectedProps?.currentUserPrivilegeSet ?? "");
1804
- const readOnly = privileges.includes("privilege") && !/"(write|writeContent|all)"/.test(privileges);
1805
- return { id: c.url, accountId, name, color: color2, readOnly };
1806
- }
1807
- var CALENDAR_PROPS = {
1808
- "d:displayname": {},
1809
- "ca:calendar-color": {},
1810
- "cs:getctag": {},
1811
- "d:resourcetype": {},
1812
- "c:supported-calendar-component-set": {},
1813
- "d:sync-token": {},
1814
- "d:current-user-privilege-set": {}
1815
- };
1816
- async function listVeventCalendars(conn) {
1817
- const cals = await fetchCalendars({
1818
- account: conn.account,
1819
- headers: conn.headers,
1820
- props: CALENDAR_PROPS,
1821
- projectedProps: { currentUserPrivilegeSet: true },
1822
- fetch: davFetch
1823
- });
1824
- return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
1825
- }
1826
- var davFetch = async (input, init) => {
1827
- const res = await timedFetch(input, init);
1828
- if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
1829
- return res;
1830
- };
1831
- function check(res, what) {
1832
- if (res.ok) return;
1833
- if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
1834
- throw new Error(`${what} failed: ${res.status} ${res.statusText}`);
1835
- }
1836
- var createCaldavProvider = (ctx) => {
1837
- if (ctx.credentials.kind !== "caldav") throw new Error("CalDAV provider needs caldav credentials");
1838
- const { serverUrl, username, password } = ctx.credentials;
1839
- let conn;
1840
- const getConn = () => {
1841
- conn ??= connect2(serverUrl, username, password).catch((e) => {
1842
- conn = void 0;
1843
- throw e;
1844
- });
1845
- return conn;
1846
- };
1847
- const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
1848
- async function reload(calendarId, href, id2, around) {
1849
- const { headers } = await getConn();
1850
- const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
1851
- if (!obj?.data) throw new Error("Saved event could not be read back from the server");
1852
- const range = { start: new Date(new Date(around.start).getTime() - DAY).toISOString(), end: new Date(new Date(around.end).getTime() + DAY).toISOString() };
1853
- const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
1854
- const at3 = Date.parse(around.start);
1855
- const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
1856
- if (!ev) throw new Error("Saved event not found on the server");
1857
- return ev;
1858
- }
1859
- const put = async (event, ics, what) => {
1860
- const { headers } = await getConn();
1861
- const raw = event.raw;
1862
- check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
1863
- };
1864
- const provider = {
1865
- async listCalendars() {
1866
- const c = await getConn();
1867
- return (await listVeventCalendars(c)).map((cal) => toCalendar(cal, ctx.accountId));
1868
- },
1869
- async listEvents(calendarId, range) {
1870
- const { headers } = await getConn();
1871
- const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
1872
- return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
1873
- },
1874
- async createEvent(calendarId, input) {
1875
- const { headers } = await getConn();
1876
- const uid = randomUUID4();
1877
- const filename = `${uid}.ics`;
1878
- const ics = buildIcs(uid, input, ctx.email);
1879
- check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
1880
- const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
1881
- return reload(calendarId, href, void 0, input);
1882
- },
1883
- async updateEvent(event, scope = "one") {
1884
- const raw = event.raw;
1885
- if (!raw.recurrenceId || scope === "one") {
1886
- await put(event, applyUpdate(event, ctx.email), "Update event");
1887
- return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
1888
- }
1889
- const uid = randomUUID4();
1890
- const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
1891
- if (!split) {
1892
- await put(event, applyUpdateSeries(event, ctx.email), "Update event");
1893
- return reload(event.calendarId, raw.href, void 0, event);
1894
- }
1895
- await put(event, split.rest, "Update event");
1896
- const { headers } = await getConn();
1897
- const filename = `${uid}.ics`;
1898
- check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
1899
- const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
1900
- return reload(event.calendarId, href, void 0, event);
1901
- },
1902
- async deleteEvent(event, scope = "one") {
1903
- const raw = event.raw;
1904
- const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
1905
- if (rest) return put(event, rest, "Delete event");
1906
- const { headers } = await getConn();
1907
- check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
1908
- },
1909
- async getRecurrence(event) {
1910
- return recurrenceOf(event.raw);
1911
- },
1912
- async respond(event, status) {
1913
- await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
1914
- return reload(event.calendarId, event.raw.href, event.id, event);
1915
- }
1916
- };
1917
- return provider;
1918
- };
1919
- async function verifyCaldav(input) {
1920
- const c = await connect2(input.serverUrl.trim(), input.username, input.password);
1921
- await listVeventCalendars(c);
1922
- const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
1923
- const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
1924
- const login = cleanEmail(input.username);
1925
- const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
1926
- if (!email) throw new Error("Could not determine the account email; use your full email address as the username");
1927
- return { email };
1928
- }
1929
-
1930
- // ../../packages/core/src/providers/google/oauth.ts
1931
- import { createHash as createHash2, randomBytes } from "crypto";
1932
- import { createServer } from "http";
1933
- var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
1934
- var TOKEN_URL = "https://oauth2.googleapis.com/token";
1935
- var SCOPES = "openid email https://www.googleapis.com/auth/calendar.calendarlist.readonly https://www.googleapis.com/auth/calendar.events";
1936
- var TIMEOUT_MS = 5 * 60 * 1e3;
1937
- var clientConfig = {};
1938
- function setClientConfig(cfg) {
1939
- clientConfig = cfg;
1940
- }
1941
- function getClientConfig() {
1942
- const { clientId, clientSecret } = clientConfig;
1943
- if (!clientId || !clientSecret) throw new Error("Google sign-in is not configured in this build: add a CalDAV account, or build from source with MYSTICALS_GOOGLE_CLIENT_ID/SECRET set");
1944
- return { clientId, clientSecret };
1945
- }
1946
- function createPkce() {
1947
- const verifier = randomBytes(32).toString("base64url");
1948
- return { verifier, challenge: createHash2("sha256").update(verifier).digest("base64url") };
1949
- }
1950
- function buildAuthUrl(p) {
1951
- const url = new URL(AUTH_URL);
1952
- url.search = new URLSearchParams({
1953
- client_id: p.clientId,
1954
- redirect_uri: p.redirectUri,
1955
- response_type: "code",
1956
- scope: SCOPES,
1957
- state: p.state,
1958
- code_challenge: p.codeChallenge,
1959
- code_challenge_method: "S256",
1960
- access_type: "offline",
1961
- prompt: "consent"
1962
- }).toString();
1963
- return url.toString();
2823
+ r.until = until;
2824
+ p.setValue(r);
2825
+ }
2826
+ master.updatePropertyWithValue("sequence", Number(master.getFirstPropertyValue("sequence") ?? 0) + 1);
2827
+ touch(master);
2828
+ return root.toString();
1964
2829
  }
1965
- function emailFromIdToken(idToken) {
1966
- try {
1967
- const payload = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString("utf8"));
1968
- return typeof payload.email === "string" ? payload.email : void 0;
1969
- } catch {
1970
- return void 0;
2830
+ function applyRespond(raw, email, status) {
2831
+ const root = parse(raw.ics);
2832
+ let found = false;
2833
+ for (const v of root.getAllSubcomponents("vevent")) {
2834
+ for (const p of v.getAllProperties("attendee")) {
2835
+ if (!sameEmail(p.getFirstValue(), email)) continue;
2836
+ p.setParameter("partstat", PARTSTAT_OUT[status]);
2837
+ p.removeParameter("rsvp");
2838
+ p.removeParameter("schedule-status");
2839
+ found = true;
2840
+ }
2841
+ if (found) v.updatePropertyWithValue("dtstamp", ICAL.Time.now());
1971
2842
  }
2843
+ if (!found) throw new Error("This account is not an attendee of the event");
2844
+ return root.toString();
1972
2845
  }
1973
- function parseTokenResponse(json, now = Date.now()) {
1974
- const j = json ?? {};
1975
- if (typeof j.error === "string") {
1976
- const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
1977
- throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
2846
+
2847
+ // ../../packages/core/src/providers/caldav/index.ts
2848
+ var FALLBACK_COLOR = "#8e8e93";
2849
+ var DAY2 = 864e5;
2850
+ async function connect2(serverUrl, username, password) {
2851
+ try {
2852
+ new URL(serverUrl);
2853
+ } catch {
2854
+ throw new Error(`Invalid server URL: ${serverUrl}`);
1978
2855
  }
1979
- if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
1980
- const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
1981
- return {
1982
- accessToken: j.access_token,
1983
- expiresAt: now + expiresIn * 1e3,
1984
- refreshToken: typeof j.refresh_token === "string" ? j.refresh_token : void 0,
1985
- email: typeof j.id_token === "string" ? emailFromIdToken(j.id_token) : void 0
2856
+ const headers = getBasicAuthHeaders({ username, password });
2857
+ let unauthorized = false;
2858
+ const trackingFetch = async (input, init) => {
2859
+ const res = await timedFetch(input, init);
2860
+ if (res.status === 401) unauthorized = true;
2861
+ return res;
1986
2862
  };
1987
- }
1988
- async function postToken(params) {
1989
- const res = await timedFetch(TOKEN_URL, {
1990
- method: "POST",
1991
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1992
- body: new URLSearchParams(params).toString()
1993
- });
1994
- const json = await res.json().catch(() => ({ error: `http_${res.status}` }));
1995
- if (!res.ok && !json.error) json.error = `http_${res.status}`;
1996
- return parseTokenResponse(json);
1997
- }
1998
- var DONE_HTML = (msg, returnUrl) => `<!doctype html><meta charset="utf-8"><title>Mysticals</title><body style="font:15px -apple-system,sans-serif;text-align:center;padding-top:80px"><h2>${msg}</h2>` + (returnUrl ? `<p>Returning to Mysticals\u2026</p><p><a href="${returnUrl}" style="display:inline-block;padding:8px 16px;border-radius:6px;background:#bd93f9;color:#0b0b10;font-weight:600;text-decoration:none">Open Mysticals</a></p><p style="color:#888">You can close this tab.</p><script>location.href=${JSON.stringify(returnUrl)}</script></body>` : `<p>You can close this tab and return to Mysticals.</p></body>`);
1999
- async function runOAuthFlow(openUrl3, opts = {}) {
2000
- const cfg = getClientConfig();
2001
- const { verifier, challenge } = createPkce();
2002
- const state = randomBytes(16).toString("base64url");
2003
- const server = createServer();
2004
2863
  try {
2005
- await new Promise((resolve, reject) => {
2006
- server.once("error", reject);
2007
- server.listen(0, "127.0.0.1", resolve);
2008
- });
2009
- const redirectUri = `http://127.0.0.1:${server.address().port}`;
2010
- const code = await new Promise((resolve, reject) => {
2011
- const timer = setTimeout(() => reject(new Error("Google sign-in timed out")), TIMEOUT_MS);
2012
- const finish = (err, value) => {
2013
- clearTimeout(timer);
2014
- if (err) reject(err);
2015
- else resolve(value);
2016
- };
2017
- server.on("request", (req, res) => {
2018
- const params = new URL(req.url ?? "/", redirectUri).searchParams;
2019
- const reply = (status, msg, then) => {
2020
- res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
2021
- res.end(DONE_HTML(msg, status === 200 ? opts.returnUrl : void 0), then);
2022
- };
2023
- if (!params.has("code") && !params.has("error")) {
2024
- res.writeHead(404, { Connection: "close" }).end();
2025
- return;
2026
- }
2027
- if (params.get("state") !== state) {
2028
- reply(400, "Sign-in failed", () => finish(new Error("Google sign-in failed: state mismatch")));
2029
- } else if (params.has("error")) {
2030
- reply(400, "Sign-in cancelled", () => finish(new Error(`Google sign-in failed: ${params.get("error")}`)));
2031
- } else {
2032
- const code2 = params.get("code");
2033
- reply(200, "Signed in", () => finish(null, code2));
2034
- }
2035
- });
2036
- openUrl3(buildAuthUrl({ clientId: cfg.clientId, redirectUri, state, codeChallenge: challenge })).catch(
2037
- (e) => finish(e instanceof Error ? e : new Error(String(e)))
2038
- );
2039
- });
2040
- opts.onCode?.();
2041
- const tok = await postToken({
2042
- grant_type: "authorization_code",
2043
- code,
2044
- code_verifier: verifier,
2045
- redirect_uri: redirectUri,
2046
- client_id: cfg.clientId,
2047
- client_secret: cfg.clientSecret
2864
+ const account = await createAccount({
2865
+ account: { serverUrl, accountType: "caldav", credentials: { username, password } },
2866
+ headers,
2867
+ fetch: trackingFetch
2048
2868
  });
2049
- if (!tok.refreshToken) throw new Error("Google did not return a refresh token");
2050
- const email = tok.email ?? await fetchUserEmail(tok.accessToken);
2051
- return { email, credentials: { kind: "google", refreshToken: tok.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt } };
2052
- } finally {
2053
- server.closeAllConnections();
2054
- server.close();
2869
+ return { account, headers };
2870
+ } catch (e) {
2871
+ if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
2872
+ throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
2055
2873
  }
2056
2874
  }
2057
- async function fetchUserEmail(accessToken) {
2058
- const res = await timedFetch("https://openidconnect.googleapis.com/v1/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
2059
- const json = await res.json().catch(() => ({}));
2060
- if (!res.ok || typeof json.email !== "string") throw new Error("Could not read Google account email");
2061
- return json.email;
2062
- }
2063
-
2064
- // ../../packages/core/src/providers/google/provider.ts
2065
- var API = "https://www.googleapis.com/calendar/v3";
2066
- var STATUSES = ["accepted", "declined", "tentative", "needsAction"];
2067
- var toStatus = (s) => STATUSES.includes(s) ? s : "needsAction";
2068
- function mapEvent(g, accountId, calendarId) {
2069
- const allDay = !!g.start.date;
2070
- const time = (t) => allDay ? t.date : new Date(t.dateTime).toISOString();
2071
- const attendees = (g.attendees ?? []).map((a) => ({
2072
- email: a.email,
2073
- name: a.displayName,
2074
- status: toStatus(a.responseStatus),
2075
- self: a.self || void 0,
2076
- organizer: a.organizer || void 0
2077
- }));
2078
- const self = g.attendees?.find((a) => a.self);
2079
- return {
2080
- id: g.id,
2081
- accountId,
2082
- calendarId,
2083
- title: g.summary ?? "",
2084
- start: time(g.start),
2085
- end: time(g.end),
2086
- allDay,
2087
- location: g.location,
2088
- description: g.description,
2089
- organizer: g.organizer?.email ? { email: g.organizer.email, name: g.organizer.displayName } : void 0,
2090
- attendees,
2091
- myStatus: self ? toStatus(self.responseStatus) : void 0,
2092
- etag: g.etag,
2093
- raw: g,
2094
- recurringEventId: g.recurringEventId
2095
- };
2875
+ function toCalendar(c, accountId) {
2876
+ const name = typeof c.displayName === "string" && c.displayName ? c.displayName : decodeURIComponent(c.url.split("/").filter(Boolean).pop() ?? c.url);
2877
+ const color2 = typeof c.calendarColor === "string" && /^#[0-9a-f]{6}/i.test(c.calendarColor) ? c.calendarColor.slice(0, 7) : FALLBACK_COLOR;
2878
+ const privileges = JSON.stringify(c.projectedProps?.currentUserPrivilegeSet ?? "");
2879
+ const readOnly = privileges.includes("privilege") && !/"(write|writeContent|all)"/.test(privileges);
2880
+ return { id: c.url, accountId, name, color: color2, readOnly };
2096
2881
  }
2097
- function truncateRecurrence(recurrence, cutoff) {
2098
- const until = cutoff.date ? new Date(Date.parse(cutoff.date) - 864e5).toISOString().slice(0, 10).replace(/-/g, "") : new Date(Date.parse(cutoff.dateTime) - 1e3).toISOString().replace(/[-:]|\.\d{3}/g, "");
2099
- return recurrence.map((line) => {
2100
- if (!line.startsWith("RRULE:")) return line;
2101
- const parts = line.slice(6).split(";").filter((p) => !/^(COUNT|UNTIL)=/i.test(p));
2102
- return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
2882
+ var CALENDAR_PROPS = {
2883
+ "d:displayname": {},
2884
+ "ca:calendar-color": {},
2885
+ "cs:getctag": {},
2886
+ "d:resourcetype": {},
2887
+ "c:supported-calendar-component-set": {},
2888
+ "d:sync-token": {},
2889
+ "d:current-user-privilege-set": {}
2890
+ };
2891
+ async function listVeventCalendars(conn) {
2892
+ const cals = await fetchCalendars({
2893
+ account: conn.account,
2894
+ headers: conn.headers,
2895
+ props: CALENDAR_PROPS,
2896
+ projectedProps: { currentUserPrivilegeSet: true },
2897
+ fetch: davFetch
2103
2898
  });
2104
- }
2105
- var DAY2 = 864e5;
2106
- var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY2).toISOString().slice(0, 10);
2107
- var daysBetween2 = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY2);
2108
- var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
2109
- function shiftAnchor(anchor, before, start, allDay, timeZone) {
2110
- if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween2(before.date ?? before.dateTime, start)) };
2111
- if (anchor.dateTime && before.dateTime) {
2112
- return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
2113
- }
2114
- const at3 = new Date(start);
2115
- const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween2(before.date, localDate(at3)))}T00:00:00`);
2116
- day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
2117
- return { dateTime: day.toISOString(), timeZone };
2118
- }
2119
- function endFrom(from, event, timeZone) {
2120
- if (event.allDay) return { date: plusDays(from.date, daysBetween2(event.start, event.end)) };
2121
- return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
2122
- }
2123
- var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
2124
- var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
2125
- var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
2126
- var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
2127
- var nulled = (t) => ({ date: null, dateTime: null, timeZone: null, ...t });
2128
- function createGoogleProviderImpl(ctx) {
2129
- if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
2130
- let creds = ctx.credentials;
2131
- let refreshing;
2132
- const refresh = () => refreshing ??= (async () => {
2133
- const { clientId, clientSecret } = getClientConfig();
2134
- const tok = await postToken({ grant_type: "refresh_token", refresh_token: creds.refreshToken, client_id: clientId, client_secret: clientSecret });
2135
- creds = { kind: "google", refreshToken: tok.refreshToken ?? creds.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt };
2136
- await ctx.saveCredentials(creds);
2137
- return tok.accessToken;
2138
- })().finally(() => refreshing = void 0);
2139
- const token = async () => creds.accessToken && (creds.expiresAt ?? 0) > Date.now() + 6e4 ? creds.accessToken : refresh();
2140
- async function api(method, path, query = {}, body) {
2141
- const qs = new URLSearchParams(query).toString();
2142
- const url = `${API}${path}${qs ? `?${qs}` : ""}`;
2143
- const send = async (accessToken) => timedFetch(url, {
2144
- method,
2145
- headers: { Authorization: `Bearer ${accessToken}`, ...body ? { "Content-Type": "application/json" } : {} },
2146
- body: body ? JSON.stringify(body) : void 0
2147
- });
2148
- let res = await send(await token());
2149
- if (res.status === 401) res = await send(await refresh());
2150
- if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
2151
- if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
2152
- if (!res.ok) {
2153
- const err = await res.json().catch(() => ({}));
2154
- throw new Error(`Google API ${res.status}: ${err.error?.message ?? res.statusText}`);
2155
- }
2156
- return await res.json();
2157
- }
2158
- const eventPath = (calendarId, eventId) => `/calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`;
2159
- const rawOf = (e) => e.raw;
2160
- const isOrganizer = (e) => {
2161
- const org = rawOf(e)?.organizer;
2162
- if (org?.self !== void 0) return org.self;
2163
- return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
2164
- };
2165
- const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
2166
- const fields = (event, prevAttendees, organizer) => {
2167
- const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
2168
- if (organizer) {
2169
- const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
2170
- body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
2171
- }
2172
- return body;
2899
+ return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
2900
+ }
2901
+ var davFetch = async (input, init) => {
2902
+ const res = await timedFetch(input, init);
2903
+ if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
2904
+ return res;
2905
+ };
2906
+ function check(res, what) {
2907
+ if (res.ok) return;
2908
+ if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
2909
+ throw new Error(`${what} failed: ${res.status} ${res.statusText}`);
2910
+ }
2911
+ var createCaldavProvider = (ctx) => {
2912
+ if (ctx.credentials.kind !== "caldav") throw new Error("CalDAV provider needs caldav credentials");
2913
+ const { serverUrl, username, password } = ctx.credentials;
2914
+ let conn;
2915
+ const getConn = () => {
2916
+ conn ??= connect2(serverUrl, username, password).catch((e) => {
2917
+ conn = void 0;
2918
+ throw e;
2919
+ });
2920
+ return conn;
2173
2921
  };
2174
- async function remainingRecurrence(calendarId, series, recurrence, slot) {
2175
- const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
2176
- if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
2177
- const cut = Date.parse(slot.dateTime ?? slot.date);
2178
- let passed = 0;
2179
- let pageToken;
2180
- do {
2181
- const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
2182
- showDeleted: "true",
2183
- timeMax: new Date(cut).toISOString(),
2184
- maxResults: "2500",
2185
- ...pageToken ? { pageToken } : {}
2186
- });
2187
- for (const g of page.items ?? []) {
2188
- const t = g.originalStartTime ?? g.start;
2189
- if (Date.parse(t.dateTime ?? t.date) < cut) passed++;
2190
- }
2191
- pageToken = page.nextPageToken;
2192
- } while (pageToken);
2193
- return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
2922
+ const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
2923
+ async function reload(calendarId, href, id2, around) {
2924
+ const { headers } = await getConn();
2925
+ const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
2926
+ if (!obj?.data) throw new Error("Saved event could not be read back from the server");
2927
+ const range = { start: new Date(new Date(around.start).getTime() - DAY2).toISOString(), end: new Date(new Date(around.end).getTime() + DAY2).toISOString() };
2928
+ const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
2929
+ const at3 = Date.parse(around.start);
2930
+ const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
2931
+ if (!ev) throw new Error("Saved event not found on the server");
2932
+ return ev;
2194
2933
  }
2195
- return {
2934
+ const put = async (event, ics, what) => {
2935
+ const { headers } = await getConn();
2936
+ const raw = event.raw;
2937
+ check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
2938
+ };
2939
+ const provider = {
2196
2940
  async listCalendars() {
2197
- const out = [];
2198
- let pageToken;
2199
- do {
2200
- const page = await api(
2201
- "GET",
2202
- "/users/me/calendarList",
2203
- pageToken ? { pageToken } : {}
2204
- );
2205
- for (const c of page.items ?? [])
2206
- out.push({
2207
- id: c.id,
2208
- accountId: ctx.accountId,
2209
- name: c.summaryOverride ?? c.summary ?? c.id,
2210
- color: c.backgroundColor ?? "#4285f4",
2211
- readOnly: c.accessRole !== "owner" && c.accessRole !== "writer"
2212
- });
2213
- pageToken = page.nextPageToken;
2214
- } while (pageToken);
2215
- return out;
2941
+ const c = await getConn();
2942
+ return (await listVeventCalendars(c)).map((cal) => toCalendar(cal, ctx.accountId));
2216
2943
  },
2217
2944
  async listEvents(calendarId, range) {
2218
- const out = [];
2219
- let pageToken;
2220
- do {
2221
- const page = await api("GET", eventPath(calendarId), {
2222
- singleEvents: "true",
2223
- timeMin: new Date(range.start).toISOString(),
2224
- timeMax: new Date(range.end).toISOString(),
2225
- maxResults: "2500",
2226
- ...pageToken ? { pageToken } : {}
2227
- });
2228
- for (const g of page.items ?? []) if (g.status !== "cancelled") out.push(map(g, calendarId));
2229
- pageToken = page.nextPageToken;
2230
- } while (pageToken);
2231
- return out;
2945
+ const { headers } = await getConn();
2946
+ const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
2947
+ return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
2232
2948
  },
2233
2949
  async createEvent(calendarId, input) {
2234
- const attendees = (input.attendees ?? []).map((email) => ({ email }));
2235
- const rule = input.recurrence;
2236
- const g = await api(
2237
- "POST",
2238
- eventPath(calendarId),
2239
- { sendUpdates: attendees.length ? "all" : "none" },
2240
- {
2241
- summary: input.title,
2242
- start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
2243
- end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
2244
- location: input.location,
2245
- description: input.description,
2246
- attendees,
2247
- ...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
2248
- }
2249
- );
2250
- return map(g, calendarId);
2950
+ const { headers } = await getConn();
2951
+ const uid = randomUUID4();
2952
+ const filename = `${uid}.ics`;
2953
+ const ics = buildIcs(uid, input, ctx.email);
2954
+ check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
2955
+ const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
2956
+ return reload(calendarId, href, void 0, input);
2251
2957
  },
2252
2958
  async updateEvent(event, scope = "one") {
2253
- const organizer = isOrganizer(event);
2254
- const q = { sendUpdates: organizer ? "all" : "none" };
2255
- const series = event.recurringEventId;
2256
- const before = rawOf(event)?.start;
2257
- if (!series || scope === "one" || !before) {
2258
- const rule2 = !series ? event.recurrence : void 0;
2259
- const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
2260
- const body = {
2261
- ...fields(event, rawOf(event)?.attendees, organizer),
2262
- start: time(event.start),
2263
- end: time(event.end),
2264
- ...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
2265
- };
2266
- return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
2959
+ const raw = event.raw;
2960
+ if (!raw.recurrenceId || scope === "one") {
2961
+ await put(event, applyUpdate(event, ctx.email), "Update event");
2962
+ return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
2267
2963
  }
2268
- const master = await api("GET", eventPath(event.calendarId, series));
2269
- const timeZone = master.start.timeZone ?? localZone();
2270
- const rule = event.recurrence;
2271
- const slot = rawOf(event)?.originalStartTime ?? before;
2272
- const first = master.start.dateTime ?? master.start.date;
2273
- const at3 = slot.dateTime ?? slot.date;
2274
- if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
2275
- const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
2276
- const body = {
2277
- ...fields(event, master.attendees, organizer),
2278
- start: nulled(start2),
2279
- end: nulled(endFrom(start2, event, timeZone)),
2280
- // A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
2281
- ...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
2282
- };
2283
- await api("PATCH", eventPath(event.calendarId, series), q, body);
2284
- return event;
2964
+ const uid = randomUUID4();
2965
+ const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
2966
+ if (!split) {
2967
+ await put(event, applyUpdateSeries(event, ctx.email), "Update event");
2968
+ return reload(event.calendarId, raw.href, void 0, event);
2285
2969
  }
2286
- const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
2287
- await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
2288
- const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
2289
- const created = await api("POST", eventPath(event.calendarId), q, {
2290
- ...fields(event, master.attendees, organizer),
2291
- start,
2292
- end: endFrom(start, event, timeZone),
2293
- ...recurrence ? { recurrence } : {}
2294
- });
2295
- return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
2970
+ await put(event, split.rest, "Update event");
2971
+ const { headers } = await getConn();
2972
+ const filename = `${uid}.ics`;
2973
+ check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
2974
+ const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
2975
+ return reload(event.calendarId, href, void 0, event);
2296
2976
  },
2297
2977
  async deleteEvent(event, scope = "one") {
2298
- const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
2299
- const series = event.recurringEventId;
2300
- if (!series || scope === "one") return void await api("DELETE", eventPath(event.calendarId, event.id), q);
2301
- const cutoff = rawOf(event)?.originalStartTime;
2302
- if (scope === "following" && cutoff) {
2303
- const master = await api("GET", eventPath(event.calendarId, series));
2304
- const first = master.start.dateTime ?? master.start.date;
2305
- const at3 = cutoff.dateTime ?? cutoff.date;
2306
- if (Date.parse(at3) > Date.parse(first)) {
2307
- const recurrence = truncateRecurrence(master.recurrence ?? [], cutoff);
2308
- return void await api("PATCH", eventPath(event.calendarId, series), q, { recurrence });
2309
- }
2310
- }
2311
- await api("DELETE", eventPath(event.calendarId, series), q);
2978
+ const raw = event.raw;
2979
+ const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
2980
+ if (rest) return put(event, rest, "Delete event");
2981
+ const { headers } = await getConn();
2982
+ check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
2312
2983
  },
2313
2984
  async getRecurrence(event) {
2314
- if (!event.recurringEventId) return null;
2315
- const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
2316
- const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
2317
- return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
2985
+ return recurrenceOf(event.raw);
2318
2986
  },
2319
2987
  async respond(event, status) {
2320
- const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
2321
- if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
2322
- const g = await api(
2323
- "PATCH",
2324
- eventPath(event.calendarId, event.id),
2325
- { sendUpdates: "all" },
2326
- { attendees: attendees.map((a) => a.self ? { ...a, responseStatus: status } : a) }
2327
- );
2328
- return map(g, event.calendarId);
2988
+ await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
2989
+ return reload(event.calendarId, event.raw.href, event.id, event);
2329
2990
  }
2330
2991
  };
2992
+ return provider;
2993
+ };
2994
+ async function verifyCaldav(input) {
2995
+ const c = await connect2(input.serverUrl.trim(), input.username, input.password);
2996
+ await listVeventCalendars(c);
2997
+ const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
2998
+ const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
2999
+ const login = cleanEmail(input.username);
3000
+ const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
3001
+ if (!email) throw new Error("Could not determine the account email; use your full email address as the username");
3002
+ return { email };
2331
3003
  }
2332
3004
 
2333
3005
  // ../../packages/core/src/providers/google/index.ts
@@ -2661,7 +3333,7 @@ import {
2661
3333
  addDays as addDays4,
2662
3334
  addMonths as addMonths2,
2663
3335
  addWeeks,
2664
- format as format5,
3336
+ format as format6,
2665
3337
  isSameDay as isSameDay2,
2666
3338
  parseISO as parseISO5,
2667
3339
  startOfDay,
@@ -2746,11 +3418,11 @@ function shiftDate(view, date, dir) {
2746
3418
  if (view === "week") return addWeeks(date, dir);
2747
3419
  return addMonths2(date, dir);
2748
3420
  }
2749
- function rangeLabel(a, b) {
3421
+ function rangeLabel(a, b, locale = "en") {
2750
3422
  const head = a.getFullYear() !== b.getFullYear() ? "d MMM yyyy" : a.getMonth() !== b.getMonth() ? "d MMM" : "d";
2751
- return `${format5(a, head)} \u2013 ${format5(b, "d MMM yyyy")}`;
3423
+ return `${fmt(locale, a, head)} \u2013 ${fmt(locale, b, "d MMM yyyy")}`;
2752
3424
  }
2753
- var ymd = (d) => format5(d, "yyyy-MM-dd");
3425
+ var ymd = (d) => format6(d, "yyyy-MM-dd");
2754
3426
 
2755
3427
  // ../../packages/core/src/logic/visible.ts
2756
3428
  var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
@@ -2829,8 +3501,8 @@ function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() })
2829
3501
  function useNow(ms = 3e4) {
2830
3502
  const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
2831
3503
  useEffect(() => {
2832
- const t = setInterval(() => setNow(/* @__PURE__ */ new Date()), ms);
2833
- return () => clearInterval(t);
3504
+ const t2 = setInterval(() => setNow(/* @__PURE__ */ new Date()), ms);
3505
+ return () => clearInterval(t2);
2834
3506
  }, [ms]);
2835
3507
  return now;
2836
3508
  }
@@ -2852,9 +3524,9 @@ import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink
2852
3524
  import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2853
3525
 
2854
3526
  // ../../packages/core/src/logic/editor.ts
2855
- import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
3527
+ import { addDays as addDays6, addHours, format as format7, parseISO as parseISO6, startOfHour } from "date-fns";
2856
3528
  var LOCAL = "yyyy-MM-dd'T'HH:mm";
2857
- var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
3529
+ var toLocalInput = (iso2) => format7(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
2858
3530
  var fromLocalInput = (local) => new Date(local).toISOString();
2859
3531
  var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
2860
3532
  var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
@@ -2864,8 +3536,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
2864
3536
  var isEmail = (s) => EMAIL.test(s);
2865
3537
  function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
2866
3538
  const accountId = soleId(writableAccounts(accounts, calendars));
2867
- const start = prefill.start ? toLocalInput(prefill.start) : format6(startOfHour(addHours(now, 1)), LOCAL);
2868
- let end = prefill.end ? toLocalInput(prefill.end) : format6(addHours(new Date(start), 1), LOCAL);
3539
+ const start = prefill.start ? toLocalInput(prefill.start) : format7(startOfHour(addHours(now, 1)), LOCAL);
3540
+ let end = prefill.end ? toLocalInput(prefill.end) : format7(addHours(new Date(start), 1), LOCAL);
2869
3541
  if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
2870
3542
  return {
2871
3543
  accountId,
@@ -2882,7 +3554,7 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
2882
3554
  };
2883
3555
  }
2884
3556
  var inclusiveEnd = (startLocal, endLocal) => {
2885
- const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
3557
+ const d = format7(addDays6(new Date(endLocal), -1), LOCAL);
2886
3558
  return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
2887
3559
  };
2888
3560
  function formFromEvent(e) {
@@ -2907,53 +3579,53 @@ var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
2907
3579
  var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
2908
3580
  function setAllDay(f, allDay) {
2909
3581
  if (allDay || f.end > f.start) return { ...f, allDay };
2910
- return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
3582
+ return { ...f, allDay, end: format7(addHours(new Date(f.start), 1), LOCAL) };
2911
3583
  }
2912
3584
  function moveStart(f, start) {
2913
3585
  const end = new Date(new Date(f.end).getTime() + new Date(start).getTime() - new Date(f.start).getTime());
2914
- return { ...f, start, end: format6(end, LOCAL) };
3586
+ return { ...f, start, end: format7(end, LOCAL) };
2915
3587
  }
2916
- function times(f) {
3588
+ function times(f, locale) {
2917
3589
  if (f.allDay) {
2918
3590
  const start2 = f.start.slice(0, 10);
2919
3591
  const endIncl = f.end.slice(0, 10);
2920
- if (endIncl < start2) throw new Error("End date is before start date");
2921
- return { allDay: true, start: start2, end: format6(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
3592
+ if (endIncl < start2) throw new Error(t(locale, "form.endBeforeStartDate"));
3593
+ return { allDay: true, start: start2, end: format7(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
2922
3594
  }
2923
3595
  const start = fromLocalInput(f.start);
2924
3596
  const end = fromLocalInput(f.end);
2925
- if (end <= start) throw new Error("End must be after start");
3597
+ if (end <= start) throw new Error(t(locale, "form.endAfterStart"));
2926
3598
  return { allDay: false, start, end };
2927
3599
  }
2928
- function checkEmails(list) {
3600
+ function checkEmails(list, locale) {
2929
3601
  const bad = list.filter((e) => !isEmail(e));
2930
- if (bad.length) throw new Error(`Invalid email: ${bad.join(", ")}`);
3602
+ if (bad.length) throw new Error(t(locale, "form.invalidEmail", { list: bad.join(", ") }));
2931
3603
  return [...new Set(list.map((e) => e.toLowerCase()))];
2932
3604
  }
2933
- function formToInput(f) {
2934
- if (!f.accountId) throw new Error("Choose an account");
2935
- if (!f.calendarId) throw new Error("Choose a calendar");
2936
- const attendees = checkEmails(f.attendees);
3605
+ function formToInput(f, locale = "en") {
3606
+ if (!f.accountId) throw new Error(t(locale, "form.chooseAccount"));
3607
+ if (!f.calendarId) throw new Error(t(locale, "form.chooseCalendar"));
3608
+ const attendees = checkEmails(f.attendees, locale);
2937
3609
  return {
2938
3610
  accountId: f.accountId,
2939
3611
  calendarId: f.calendarId,
2940
- title: f.title.trim() || "New Event",
2941
- ...times(f),
3612
+ title: f.title.trim() || t(locale, "common.newEvent"),
3613
+ ...times(f, locale),
2942
3614
  location: f.location.trim() || void 0,
2943
3615
  description: f.description.trim() || void 0,
2944
3616
  ...attendees.length ? { attendees } : {},
2945
3617
  ...f.repeat ? { recurrence: f.repeat } : {}
2946
3618
  };
2947
3619
  }
2948
- function applyForm(e, f) {
2949
- const emails = checkEmails(f.attendees);
3620
+ function applyForm(e, f, locale = "en") {
3621
+ const emails = checkEmails(f.attendees, locale);
2950
3622
  const keep = e.attendees.filter((a) => a.self || a.organizer);
2951
3623
  const byEmail = new Map(e.attendees.map((a) => [a.email.toLowerCase(), a]));
2952
3624
  const invited = emails.filter((m) => !keep.some((a) => a.email.toLowerCase() === m)).map((m) => byEmail.get(m) ?? { email: m, status: "needsAction" });
2953
3625
  return {
2954
3626
  ...e,
2955
- title: f.title.trim() || "New Event",
2956
- ...times(f),
3627
+ title: f.title.trim() || t(locale, "common.newEvent"),
3628
+ ...times(f, locale),
2957
3629
  location: f.location.trim() || void 0,
2958
3630
  description: f.description.trim() || void 0,
2959
3631
  attendees: [...keep, ...invited],
@@ -3080,7 +3752,7 @@ import { Box as Box3, Text as Text3 } from "ink";
3080
3752
  import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
3081
3753
 
3082
3754
  // ../../packages/core/src/logic/details.ts
3083
- import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
3755
+ import { addDays as addDays7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
3084
3756
  var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
3085
3757
  function ownerLine(calendar, label, email) {
3086
3758
  return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
@@ -3092,15 +3764,17 @@ function canEdit(e, account, calendar) {
3092
3764
  }
3093
3765
  var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
3094
3766
  var pendingInvites = (events, now = /* @__PURE__ */ new Date()) => events.filter((e) => e.myStatus === "needsAction" && at2(e.end, e.allDay) > now.getTime()).sort((a, b) => at2(a.start, a.allDay) - at2(b.start, b.allDay));
3095
- function formatWhen(e) {
3767
+ function formatWhen(e, locale = "en") {
3768
+ const f = (d, p) => fmt(locale, d, p);
3096
3769
  if (e.allDay) {
3097
3770
  const s2 = parseISO7(e.start);
3098
3771
  const last = addDays7(parseISO7(e.end), -1);
3099
- return last > s2 ? `${format7(s2, "EEE, d MMM")} \u2013 ${format7(last, "EEE, d MMM")} \xB7 all day` : `${format7(s2, "EEE, d MMM")} \xB7 all day`;
3772
+ const allDay = t(locale, "when.allDay");
3773
+ return last > s2 ? `${f(s2, "EEE, d MMM")} \u2013 ${f(last, "EEE, d MMM")} \xB7 ${allDay}` : `${f(s2, "EEE, d MMM")} \xB7 ${allDay}`;
3100
3774
  }
3101
3775
  const s = new Date(e.start);
3102
- const t = new Date(e.end);
3103
- return isSameDay3(s, t) ? `${format7(s, "EEE, d MMM")} \xB7 ${format7(s, "HH:mm")} \u2013 ${format7(t, "HH:mm")}` : `${format7(s, "EEE, d MMM HH:mm")} \u2013 ${format7(t, "EEE, d MMM HH:mm")}`;
3776
+ const end = new Date(e.end);
3777
+ return isSameDay3(s, end) ? `${f(s, "EEE, d MMM")} \xB7 ${f(s, "HH:mm")} \u2013 ${f(end, "HH:mm")}` : `${f(s, "EEE, d MMM HH:mm")} \u2013 ${f(end, "EEE, d MMM HH:mm")}`;
3104
3778
  }
3105
3779
  var STATUS_ICON = {
3106
3780
  accepted: "\u2713",
@@ -3108,31 +3782,89 @@ var STATUS_ICON = {
3108
3782
  declined: "\u2715",
3109
3783
  needsAction: "\u2022"
3110
3784
  };
3785
+ function linkify(text2) {
3786
+ const out = [];
3787
+ const plain = (t2) => {
3788
+ if (t2) out.push({ text: t2 });
3789
+ };
3790
+ let i = 0;
3791
+ for (const m of text2.matchAll(/https?:\/\/[^\s<>"]+/gi)) {
3792
+ const url = m[0].replace(/[.,;:!?)\]}>'"]+$/, "");
3793
+ plain(text2.slice(i, m.index));
3794
+ out.push({ text: url, href: url });
3795
+ i = m.index + url.length;
3796
+ }
3797
+ plain(text2.slice(i));
3798
+ return out;
3799
+ }
3111
3800
  var cleanNotes = (s = "") => s.replace(/^@color(hex)?:.*$\n?/gim, "").trim();
3112
3801
 
3113
3802
  // ../../packages/core/src/logic/meeting.ts
3114
3803
  function meetingUrl(location) {
3115
3804
  return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
3116
3805
  }
3117
- function place(location = "") {
3118
- const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3119
- return rest || (meetingUrl(location) ? "video call" : "");
3806
+ function eventMeetingUrl(e) {
3807
+ return e.conferenceUrl ?? meetingUrl(e.location);
3808
+ }
3809
+ function locationText(location = "") {
3810
+ return location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3811
+ }
3812
+ function place(location = "", locale = "en") {
3813
+ return locationText(location) || (meetingUrl(location) ? t(locale, "meeting.videoCall") : "");
3814
+ }
3815
+ function eventPlace(e, locale = "en") {
3816
+ return place(e.location, locale) || (e.conferenceUrl ? t(locale, "meeting.videoCall") : "");
3817
+ }
3818
+ var host = (url) => {
3819
+ try {
3820
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
3821
+ } catch {
3822
+ return "";
3823
+ }
3824
+ };
3825
+ var is = (h, domain) => h === domain || h.endsWith(`.${domain}`);
3826
+ function linkKind(url) {
3827
+ const h = host(url);
3828
+ const path = url.replace(/^https?:\/\/[^/]+/i, "").toLowerCase();
3829
+ if (h === "meet.google.com") return "meet";
3830
+ if (is(h, "zoom.us") || is(h, "zoom.com") || is(h, "zoomgov.com")) return "zoom";
3831
+ if (h === "teams.microsoft.com" || h === "teams.live.com") return "teams";
3832
+ if (/^(maps|map)\./.test(h) || /^google\.[a-z.]+$/.test(h) && path.startsWith("/maps") || h === "goo.gl" && path.startsWith("/maps") || h === "maps.app.goo.gl" || is(h, "waze.com") || is(h, "openstreetmap.org"))
3833
+ return "map";
3834
+ if (/^(meet|video|call|calls|conf|jitsi|webinar)\./.test(h) || ["meet.jit.si", "whereby.com", "discord.gg", "facetime.apple.com", "gather.town", "around.co", "chime.aws"].some((d) => is(h, d)) || is(h, "webex.com") || h === "app.slack.com" && path.startsWith("/huddle"))
3835
+ return "video";
3836
+ return "link";
3837
+ }
3838
+ function linkLabel(url, kind = linkKind(url), locale = "en") {
3839
+ if (kind === "link") return host(url) || url;
3840
+ return t(locale, `link.${kind}`, kind === "video" ? { host: host(url) } : void 0);
3841
+ }
3842
+ function eventLinks(e) {
3843
+ const urls = [e.conferenceUrl, ...linkify(e.location ?? "").map((p) => p.href)];
3844
+ const seen = /* @__PURE__ */ new Set();
3845
+ const out = [];
3846
+ for (const url of urls) {
3847
+ if (!url || seen.has(url)) continue;
3848
+ seen.add(url);
3849
+ out.push({ url, kind: linkKind(url) });
3850
+ }
3851
+ return out;
3120
3852
  }
3121
3853
 
3122
3854
  // ../../packages/core/src/logic/status.ts
3123
3855
  import { differenceInMinutes, format as format8 } from "date-fns";
3124
3856
  function pickNowNext(events, now) {
3125
- const t = now.getTime();
3857
+ const t2 = now.getTime();
3126
3858
  const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
3127
3859
  return {
3128
- current: timed.filter((e) => Date.parse(e.start) <= t && t < Date.parse(e.end)),
3129
- next: timed.find((e) => Date.parse(e.start) > t)
3860
+ current: timed.filter((e) => Date.parse(e.start) <= t2 && t2 < Date.parse(e.end)),
3861
+ next: timed.find((e) => Date.parse(e.start) > t2)
3130
3862
  };
3131
3863
  }
3132
- function startsLabel(start, now) {
3864
+ function startsLabel(start, now, locale = "en") {
3133
3865
  const s = new Date(start);
3134
3866
  const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
3135
- return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
3867
+ return m <= 60 ? t(locale, "status.inMinutes", { n: m }) : t(locale, "status.at", { time: format8(s, "HH:mm") });
3136
3868
  }
3137
3869
 
3138
3870
  // src/ui/screens/EventDetails.tsx
@@ -3158,7 +3890,7 @@ function EventDetails({ event: initial, onClose, onEdit }) {
3158
3890
  const calendar = calendars.find((c) => c.accountId === event.accountId && c.id === event.calendarId);
3159
3891
  const editable = canEdit(event, account, calendar);
3160
3892
  const recurring = !!event.recurringEventId;
3161
- const url = meetingUrl(event.location);
3893
+ const url = eventMeetingUrl(event);
3162
3894
  const now = useNow();
3163
3895
  const run = (label, fn) => {
3164
3896
  setBusy(label);
@@ -3231,9 +3963,12 @@ function relative(e, now) {
3231
3963
  return +start - +now < 24 * 36e5 ? startsLabel(e.start, now) : "";
3232
3964
  }
3233
3965
  var tally = (attendees) => ["accepted", "tentative", "declined", "needsAction"].map((s) => [attendees.filter((a) => a.status === s).length, s]).filter(([n]) => n).map(([n, s]) => `${n} ${STATUS_LABEL[s]}`).join(", ");
3966
+ var LINK_GLYPH = { meet: "\u25B6", zoom: "\u25B6", teams: "\u25B6", video: "\u25B6", map: "\u2316", link: "\u2197" };
3967
+ var NAMED = /* @__PURE__ */ new Set(["meet", "zoom", "teams"]);
3234
3968
  var ATTENDEES_SHOWN = 5;
3235
3969
  function EventInfo({ event, account, calendar, now, showAll, onToggleAll }) {
3236
- const url = meetingUrl(event.location);
3970
+ const links = eventLinks(event);
3971
+ const where = locationText(event.location);
3237
3972
  const notes = cleanNotes(event.description);
3238
3973
  const owner = ownerLine(calendar?.name ?? "Calendar", account?.label ?? event.accountId, account?.email);
3239
3974
  const when2 = [duration(event), relative(event, now)].filter(Boolean).join(" \xB7 ");
@@ -3256,11 +3991,18 @@ function EventInfo({ event, account, calendar, now, showAll, onToggleAll }) {
3256
3991
  owner.email && ` \xB7 ${owner.email}`,
3257
3992
  calendar?.readOnly && " \xB7 read-only"
3258
3993
  ] }),
3259
- event.location && /* @__PURE__ */ jsxs2(Text2, { children: [
3994
+ where && /* @__PURE__ */ jsxs2(Text2, { children: [
3260
3995
  "Location: ",
3261
- event.location
3996
+ where
3262
3997
  ] }),
3263
- url && url !== event.location && /* @__PURE__ */ jsx2(Clickable, { onClick: () => openUrl2(url), children: /* @__PURE__ */ jsx2(Text2, { color: C.cyan, underline: true, children: url }) }),
3998
+ links.map((l) => /* @__PURE__ */ jsx2(Clickable, { onClick: () => openUrl2(l.url), children: /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
3999
+ /* @__PURE__ */ jsxs2(Text2, { color: l.kind === "map" || l.kind === "link" ? C.muted : C.green, children: [
4000
+ LINK_GLYPH[l.kind],
4001
+ " ",
4002
+ NAMED.has(l.kind) ? `${linkLabel(l.url, l.kind)} ` : ""
4003
+ ] }),
4004
+ /* @__PURE__ */ jsx2(Text2, { color: C.cyan, underline: true, children: l.url })
4005
+ ] }) }, l.url)),
3264
4006
  event.organizer && /* @__PURE__ */ jsxs2(Text2, { children: [
3265
4007
  "Organizer: ",
3266
4008
  event.organizer.name ? `${event.organizer.name} <${event.organizer.email}>` : event.organizer.email
@@ -3350,10 +4092,10 @@ function busyMinutes(list, day) {
3350
4092
  const iv = list.filter((e) => !e.allDay && e.myStatus !== "declined").map((e) => [Math.max(+eventBounds(e).start, from), Math.min(+eventBounds(e).end, to)]).sort((a, b) => a[0] - b[0]);
3351
4093
  let total = 0;
3352
4094
  let end = -Infinity;
3353
- for (const [s, t] of iv) {
3354
- if (t <= end) continue;
3355
- total += t - Math.max(s, end);
3356
- end = t;
4095
+ for (const [s, t2] of iv) {
4096
+ if (t2 <= end) continue;
4097
+ total += t2 - Math.max(s, end);
4098
+ end = t2;
3357
4099
  }
3358
4100
  return Math.round(total / 6e4);
3359
4101
  }
@@ -3490,7 +4232,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
3490
4232
  const details = [{ text: who, color: C.muted }];
3491
4233
  if (e.recurringEventId) details.push({ text: " \u21BB repeats", color: C.muted });
3492
4234
  if (overlaps(e, eventsOnDay(events, r.day))) details.push({ text: " \u26A0 overlaps", color: C.yellow });
3493
- const where = place(e.location);
4235
+ const where = eventPlace(e);
3494
4236
  if (where) details.push({ text: ` ${where}`, color: C.cyan });
3495
4237
  if (e.attendees.length) details.push({ text: ` ${e.attendees.length} ${e.attendees.length === 1 ? "person" : "people"}`, color: C.muted });
3496
4238
  const row2 = [
@@ -3659,7 +4401,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3659
4401
  const { start, end } = eventBounds(e);
3660
4402
  const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
3661
4403
  const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
3662
- const where = place(e.location);
4404
+ const where = eventPlace(e);
3663
4405
  const lines = colW < COMPACT_BELOW ? [p.end - p.start > 60 || start.getMinutes() === 0 ? title2 : `:${format11(start, "mm")} ${title2}`, `${from}\u2013${isSameDay6(end, day) ? format11(end, "HH:mm") : "\u2026"}`, where] : [`${from} ${title2}`, [where, duration(e)].filter(Boolean).join(" \xB7 ")];
3664
4406
  const text2 = lines[h - first] ?? "";
3665
4407
  const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
@@ -3732,8 +4474,8 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3732
4474
  // src/ui/views/Day.tsx
3733
4475
  import { jsx as jsx6 } from "react/jsx-runtime";
3734
4476
  function useDays(date, n) {
3735
- const t = startOfDay5(date).getTime();
3736
- return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
4477
+ const t2 = startOfDay5(date).getTime();
4478
+ return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t2, i)), [t2, n]);
3737
4479
  }
3738
4480
  var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
3739
4481
  var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
@@ -3742,8 +4484,8 @@ var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date,
3742
4484
  import { useMemo as useMemo4 } from "react";
3743
4485
  import { jsx as jsx7 } from "react/jsx-runtime";
3744
4486
  function Week(p) {
3745
- const t = p.date.getTime();
3746
- const days = useMemo4(() => viewDays("week", new Date(t)), [t]);
4487
+ const t2 = p.date.getTime();
4488
+ const days = useMemo4(() => viewDays("week", new Date(t2)), [t2]);
3747
4489
  return /* @__PURE__ */ jsx7(TimeGrid, { ...p, days });
3748
4490
  }
3749
4491
 
@@ -4158,8 +4900,8 @@ var FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
4158
4900
  function Spinner() {
4159
4901
  const [i, setI] = useState5(0);
4160
4902
  useEffect4(() => {
4161
- const t = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
4162
- return () => clearInterval(t);
4903
+ const t2 = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
4904
+ return () => clearInterval(t2);
4163
4905
  }, []);
4164
4906
  return /* @__PURE__ */ jsx10(Text8, { color: C.accent, children: FRAMES[i] });
4165
4907
  }
@@ -4196,9 +4938,16 @@ function StatusLine({ now, message, update, width, onOpen, onInvites, onAccounts
4196
4938
  invites === 1 ? "1 invite" : `${invites} invites`,
4197
4939
  " (i)"
4198
4940
  ] }), onInvites]);
4199
- if (failed.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4941
+ const off = failed.filter((a) => a.authError);
4942
+ const broken = failed.filter((a) => !a.authError);
4943
+ if (off.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4944
+ "\u26A0 ",
4945
+ off.map((a) => a.label).join(", "),
4946
+ " disconnected (s)"
4947
+ ] }), onAccounts]);
4948
+ if (broken.length) parts.push([/* @__PURE__ */ jsxs8(Text8, { color: C.red, children: [
4200
4949
  "\u26A0 ",
4201
- failed.map((a) => a.label).join(", "),
4950
+ broken.map((a) => a.label).join(", "),
4202
4951
  " sync failed (s)"
4203
4952
  ] }), onAccounts]);
4204
4953
  if (message) parts.push([message]);
@@ -4404,13 +5153,13 @@ function Accounts({ onClose }) {
4404
5153
  (e) => say(errorText(e), true)
4405
5154
  );
4406
5155
  };
4407
- const addGoogle = () => {
5156
+ const googleSignIn2 = (fn, done) => {
4408
5157
  const s = ++googleSession.current;
4409
5158
  setMode({ kind: "google" });
4410
5159
  setMessage(void 0);
4411
5160
  setAuthUrl(void 0);
4412
- api.accounts.addGoogle().then(
4413
- (a) => s === googleSession.current && (setMode({ kind: "list" }), say(`Added ${a.label}`)),
5161
+ fn().then(
5162
+ (a) => s === googleSession.current && (setMode({ kind: "list" }), say(done(a))),
4414
5163
  (e) => s === googleSession.current && (setMode({ kind: "list" }), say(errorText(e), true))
4415
5164
  );
4416
5165
  };
@@ -4418,7 +5167,7 @@ function Accounts({ onClose }) {
4418
5167
  const act = (input) => {
4419
5168
  const { row: row2 } = current();
4420
5169
  if (input === "R") return run("Syncing all\u2026", "Synced all", () => api.sync.now());
4421
- if (input === "g") return addGoogle();
5170
+ if (input === "g") return googleSignIn2(() => api.accounts.addGoogle(), (a2) => `Added ${a2.label}`);
4422
5171
  if (input === "a") return setMode({ kind: "caldav" });
4423
5172
  if (!row2) return;
4424
5173
  const a = row2.account;
@@ -4437,6 +5186,11 @@ function Accounts({ onClose }) {
4437
5186
  return run("Saving\u2026", `${a.label} colour changed`, () => api.accounts.update(a.id, { color: color2 }));
4438
5187
  }
4439
5188
  if (input === "x") return setMode({ kind: "confirm", account: a });
5189
+ if (input === "p" && a.authError) {
5190
+ if (a.kind === "google") return googleSignIn2(() => api.accounts.reauth(a.id), () => `${a.label} reconnected`);
5191
+ setMessage(void 0);
5192
+ return setMode({ kind: "reauth", account: a, value: "" });
5193
+ }
4440
5194
  };
4441
5195
  const confirmRemove = (yes) => {
4442
5196
  const mode2 = getMode();
@@ -4462,6 +5216,19 @@ function Accounts({ onClose }) {
4462
5216
  return;
4463
5217
  }
4464
5218
  if (mode2.kind === "confirm") return confirmRemove(input.toLowerCase() === "y");
5219
+ if (mode2.kind === "reauth") {
5220
+ if (key3.escape) return setMode({ kind: "list" });
5221
+ if (key3.return) {
5222
+ if (!mode2.value) return;
5223
+ const { id: id2, label } = mode2.account;
5224
+ const password = mode2.value;
5225
+ run("Reconnecting\u2026", `${label} reconnected`, () => api.accounts.reauth(id2, { password }));
5226
+ return setMode({ kind: "list" });
5227
+ }
5228
+ const value = editText(mode2.value, input, key3);
5229
+ if (value !== void 0) setMode({ ...mode2, value });
5230
+ return;
5231
+ }
4465
5232
  if (mode2.kind === "rename") {
4466
5233
  if (key3.escape) return setMode({ kind: "list" });
4467
5234
  if (key3.return) {
@@ -4540,7 +5307,14 @@ function Accounts({ onClose }) {
4540
5307
  /* @__PURE__ */ jsx12(Spinner, {}),
4541
5308
  " Loading calendars\u2026"
4542
5309
  ] }),
4543
- a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` })
5310
+ a.authError ? /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Disconnected: ${a.error ?? "credentials rejected"} \u2014 p to reconnect` }) : a.error && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: ` Last sync failed: ${a.error}` }),
5311
+ mode.kind === "reauth" && mode.account.id === a.id && /* @__PURE__ */ jsxs10(Text10, { children: [
5312
+ " New app password: ",
5313
+ /* @__PURE__ */ jsxs10(Text10, { color: C.cyan, children: [
5314
+ "\u2022".repeat(mode.value.length),
5315
+ "\u258F"
5316
+ ] })
5317
+ ] })
4544
5318
  ] }, a.id);
4545
5319
  }),
4546
5320
  mode.kind === "confirm" && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginTop: 1, children: [
@@ -4569,7 +5343,7 @@ function Accounts({ onClose }) {
4569
5343
  /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: `Over SSH, forward its port first: ssh -L ${loopbackPort(authUrl)}:127.0.0.1:${loopbackPort(authUrl)} <host>` })
4570
5344
  ] })
4571
5345
  ] }),
4572
- mode.kind === "rename" && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
5346
+ (mode.kind === "rename" || mode.kind === "reauth") && /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "enter save esc cancel" }),
4573
5347
  loadError && /* @__PURE__ */ jsx12(Text10, { color: C.red, children: loadError }),
4574
5348
  message && /* @__PURE__ */ jsx12(Text10, { color: message.error ? C.red : C.green, children: message.text }),
4575
5349
  mode.kind === "list" && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexWrap: "wrap", children: [
@@ -4580,10 +5354,12 @@ function Accounts({ onClose }) {
4580
5354
  row && button("e", "rename"),
4581
5355
  row && button("c", "colour"),
4582
5356
  row && button("x", "remove"),
5357
+ row?.account.authError && button("p", "reconnect"),
4583
5358
  button("g", "add Google"),
4584
5359
  button("a", "add CalDAV"),
4585
5360
  /* @__PURE__ */ jsx12(Button, { k: "esc", label: "close", onPress: onClose })
4586
- ] })
5361
+ ] }),
5362
+ mode.kind === "list" && /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "Made in Ukraine" }) })
4587
5363
  ] });
4588
5364
  }
4589
5365
  var loopbackPort = (authUrl) => {