mysticals 0.6.0 → 0.8.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 +1687 -900
  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.6.0";
8
+ var version = "0.8.0";
9
9
 
10
10
  // src/client.ts
11
11
  import { spawn } from "child_process";
@@ -210,7 +210,695 @@ 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.toggleSidebar": "Toggle Sidebar",
295
+ "toolbar.hideSidebar": "Hide sidebar",
296
+ "toolbar.showSidebar": "Show sidebar",
297
+ "menu.reload": "Reload",
298
+ "menu.devTools": "Toggle Developer Tools",
299
+ "menu.fullScreen": "Toggle Full Screen",
300
+ "menu.window": "Window",
301
+ "menu.minimize": "Minimize",
302
+ "menu.zoom": "Zoom",
303
+ "menu.front": "Bring All to Front",
304
+ "menu.openLink": "Open Link",
305
+ "menu.copyLink": "Copy Link",
306
+ "view.agenda": "Agenda",
307
+ "view.day": "Day",
308
+ "view.3day": "3 Days",
309
+ "view.week": "Week",
310
+ "view.month": "Month",
311
+ "error.noSecureStorage": "Secure storage is unavailable; refusing to store credentials",
312
+ "error.noSecureStorageLinux": "Secure storage is unavailable; refusing to store credentials. Install and unlock a Secret Service keyring (GNOME Keyring or KWallet), then restart Mysticals.",
313
+ "error.accountsTitle": "Mysticals could not load your accounts",
314
+ "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.",
315
+ "update.available": "Mysticals {version} is available",
316
+ "update.clickToUpdate": "Click to update",
317
+ "update.clickToDownload": "Click to download",
318
+ // ---- desktop: app shell and toolbar ----
319
+ "common.undo": "Undo",
320
+ "common.cancel": "Cancel",
321
+ "common.close": "Close",
322
+ "common.save": "Save",
323
+ "common.delete": "Delete",
324
+ "common.edit": "Edit",
325
+ "app.loading": "Loading",
326
+ "app.loadingAccounts": "loading accounts\u2026",
327
+ "toast.moved": "Moved \u201C{title}\u201D",
328
+ "toast.movedAll": "Moved \u201C{title}\u201D (all events)",
329
+ "toast.movedFollowing": "Moved \u201C{title}\u201D and following",
330
+ "toast.moveFailed": "Couldn't move \u201C{title}\u201D: {error}",
331
+ "scope.move": "Move recurring event",
332
+ "toolbar.weekNo": "W{n}",
333
+ "toolbar.previous": "Previous",
334
+ "toolbar.next": "Next",
335
+ "toolbar.today": "today",
336
+ "toolbar.newEventHint": "New event (N)",
337
+ "toolbar.new": "+ new",
338
+ "toolbar.syncing": "Syncing",
339
+ "toolbar.firstSync": "first sync of {names}\u2026",
340
+ // ---- desktop: settings ----
341
+ "settings.title": "Settings",
342
+ "settings.sections": "Settings sections",
343
+ "settings.tab.accounts": "Accounts",
344
+ "settings.tab.themes": "Themes",
345
+ "settings.tab.language": "Language",
346
+ "settings.tab.sync": "Sync",
347
+ "settings.tab.privacy": "Privacy",
348
+ "settings.noAccounts": "No accounts yet.",
349
+ "settings.addAccount": "Add account\u2026",
350
+ "settings.theme": "Theme",
351
+ "theme.amber": "Phosphor amber",
352
+ "theme.mono": "Black & White",
353
+ "theme.toon": "Cartoon",
354
+ "settings.language.auto": "Automatic",
355
+ "settings.language.system": "system: {name}",
356
+ "settings.sync.note": "Accounts sync automatically every 2 minutes and when the app regains focus.",
357
+ "settings.sync.syncing": "Syncing\u2026",
358
+ "settings.sync.all": "Sync all",
359
+ "settings.sync.now": "Sync now",
360
+ "settings.sync.stateSyncing": "syncing",
361
+ "settings.sync.stateError": "error",
362
+ "settings.sync.stateOk": "ok",
363
+ "settings.sync.lastFailed": "Last sync failed: {error}",
364
+ "settings.reconnect": "Reconnect",
365
+ "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.",
366
+ "settings.privacy.policy": "Privacy policy",
367
+ "settings.privacy.share": "Share anonymous usage stats",
368
+ "settings.account.labelFor": "Label for {email}",
369
+ "settings.account.colourFor": "Colour for {email}",
370
+ "settings.account.remove": "Remove",
371
+ "settings.account.removeNote": "Removes local data and credentials for {email}. Nothing is deleted on the server.",
372
+ "settings.account.removeConfirm": "Remove account",
373
+ // ---- desktop: accounts ----
374
+ "common.back": "Back",
375
+ "common.decrease": "Decrease",
376
+ "common.increase": "Increase",
377
+ "accounts.personal": "Personal",
378
+ "accounts.work": "Work",
379
+ "accounts.title": "Add calendar account",
380
+ "accounts.isolated": "Each account is isolated: events and invitations are only sent from the account they belong to.",
381
+ "accounts.google.hint": "Sign in with your browser",
382
+ "accounts.caldav.hint": "Private Email, iCloud, Fastmail\u2026",
383
+ "accounts.manage": "Manage existing accounts\u2026",
384
+ "accounts.google.waiting": "Waiting for Google sign-in in your browser\u2026",
385
+ "accounts.google.finish": "Finish signing in there; Mysticals comes back on its own.",
386
+ "accounts.step.signedInGoogle": "Signed in with Google",
387
+ "accounts.step.connecting": "Connecting your account\u2026",
388
+ "accounts.step.loadingCalendars": "Loading calendars",
389
+ "accounts.step.signingIn": "Signing in to {host}\u2026",
390
+ "accounts.field.provider": "Provider",
391
+ "accounts.provider.custom": "Custom",
392
+ "accounts.field.server": "Server URL",
393
+ "accounts.field.serverHint": "Check your provider's docs if connection fails.",
394
+ "accounts.field.email": "Email",
395
+ "accounts.field.password": "App password",
396
+ "accounts.field.label": "Label",
397
+ "accounts.field.colour": "Colour",
398
+ "accounts.field.accountColour": "Account colour",
399
+ "accounts.add": "Add account",
400
+ // ---- desktop: event editor ----
401
+ "editor.title": "Title",
402
+ "editor.account": "Account",
403
+ "editor.chooseAccount": "Choose account\u2026",
404
+ "editor.calendar": "Calendar",
405
+ "editor.chooseCalendar": "Choose calendar\u2026",
406
+ "editor.accountFirst": "Choose an account first",
407
+ "editor.allDay": "All-day",
408
+ "editor.starts": "Starts",
409
+ "editor.ends": "Ends",
410
+ "editor.location": "Location",
411
+ "editor.addLocation": "Add location",
412
+ "editor.invitees": "Invitees",
413
+ "editor.removeInvitee": "Remove {email}",
414
+ "editor.addPeople": "Add people (optional)",
415
+ "editor.notes": "Notes",
416
+ "editor.addNotes": "Add notes",
417
+ "editor.createdIn": "Created in",
418
+ "editor.invitesFrom": "Invitations will be sent from",
419
+ "editor.chooseWhich": "Choose which account this event belongs to.",
420
+ "editor.saving": "Saving\u2026",
421
+ "editor.add": "Add Event",
422
+ "scope.changeRule": "Change the repeat rule for",
423
+ "scope.save": "Save recurring event",
424
+ // ---- desktop: repeat picker ----
425
+ "scope.one": "This event",
426
+ "scope.following": "This and following",
427
+ "scope.all": "All events",
428
+ "repeat.label": "Repeat",
429
+ "repeat.loading": "Loading\u2026",
430
+ "repeat.loadFailed": "Couldn\u2019t load the repeat rule; it stays as it is.",
431
+ "repeat.customOption": "Custom\u2026",
432
+ "repeat.every": "Every",
433
+ "repeat.everyAria": "Repeat every",
434
+ "repeat.unitAria": "Repeat unit",
435
+ "repeat.unit.daily": "days",
436
+ "repeat.unit.weekly": "weeks",
437
+ "repeat.unit.monthly": "months",
438
+ "repeat.unit.yearly": "years",
439
+ "repeat.on": "On",
440
+ "repeat.onAria": "Repeat on",
441
+ "repeat.stops": "Stops",
442
+ "repeat.stopsAria": "Repeat stops",
443
+ "repeat.never": "Never",
444
+ "repeat.endOnDate": "On date",
445
+ "repeat.after": "After",
446
+ "repeat.stopsOn": "Stops on",
447
+ "repeat.occurrences": "Occurrences",
448
+ "repeat.times": "times",
449
+ // ---- desktop: date picker and event details ----
450
+ "dtf.date": "{label} date",
451
+ "dtf.time": "{label} time",
452
+ "dtf.typed": "{label} time, HH:mm",
453
+ "rsvp.accept": "Accept",
454
+ "rsvp.maybe": "Maybe",
455
+ "rsvp.decline": "Decline",
456
+ "rsvp.reply": "Reply",
457
+ "rsvp.replyAs": "Reply as {email}",
458
+ "partstat.accepted": "accepted",
459
+ "partstat.tentative": "tentative",
460
+ "partstat.declined": "declined",
461
+ "partstat.needsAction": "needs action",
462
+ "details.deleted": "This event was deleted",
463
+ "details.deletedFrom": "It was removed from {calendar}, possibly on another device.",
464
+ "details.deletedFromCalendar": "It was removed from the calendar, possibly on another device.",
465
+ "details.calendarIn": "{calendar} in",
466
+ "details.organizer": "Organizer",
467
+ "details.invitees": "Invitees ({n})",
468
+ "details.you": "you",
469
+ "details.organizerTag": "organizer",
470
+ "details.confirmDelete": "Delete this event?",
471
+ "scope.delete": "Delete recurring event",
472
+ // ---- desktop: invites, mini month, reconnect ----
473
+ "invites.title": "Invitations",
474
+ "invites.empty": "No pending invitations",
475
+ "invites.aria": "Invitations ({n})",
476
+ "invites.hint": "Invitations (i)",
477
+ "invites.count": { one: "{n} invite", other: "{n} invites" },
478
+ "mini.prev": "Previous month",
479
+ "mini.next": "Next month",
480
+ "reauth.done": "{label} reconnected",
481
+ "reauth.title": "{label} is disconnected",
482
+ "reauth.google": "Google no longer accepts the sign-in for {email}. Sign in again to reconnect.",
483
+ "reauth.caldav": "The password for {email} was changed or revoked. Enter a new app password to reconnect.",
484
+ "reauth.stayVisible": "Your calendars stay visible from the last sync until then.",
485
+ "reauth.signingIn": "Signing in\u2026",
486
+ "reauth.later": "Later",
487
+ "reauth.signInAgain": "Sign in again",
488
+ // ---- desktop: status bar and sidebar ----
489
+ "sbar.until": "until {time}",
490
+ "sbar.next": "next:",
491
+ "sbar.noUpcoming": "no upcoming events",
492
+ "sbar.updateTo": "\u2191 Update to {version}",
493
+ "sbar.download": "\u2193 Download {version}",
494
+ "sbar.downloading": "Downloading {n}%",
495
+ "sbar.restarting": "Restarting\u2026",
496
+ "sbar.updateFailed": "\u2715 update failed \xB7 retry",
497
+ "sbar.mode.agenda": "agenda",
498
+ "sbar.mode.day": "day",
499
+ "sbar.mode.3day": "3day",
500
+ "sbar.mode.week": "week",
501
+ "sbar.mode.month": "month",
502
+ "sbar.syncing": "syncing {names}",
503
+ "sbar.disconnected": "\u2715 disconnected",
504
+ "sbar.syncError": "\u2715 sync error",
505
+ "sbar.key.new": "new",
506
+ "sbar.key.today": "today",
507
+ "sbar.key.invites": "invites",
508
+ "sidebar.syncError": "Sync error: {error}",
509
+ "sidebar.loading": "Loading calendars\u2026",
510
+ "sidebar.disconnected": "Disconnected",
511
+ "sidebar.reconnect": "reconnect",
512
+ "sidebar.loadFailed": "Couldn't load calendars",
513
+ "sidebar.retry": "retry",
514
+ "sidebar.readOnlyTitle": "Read-only",
515
+ "sidebar.readOnly": "read-only",
516
+ "sidebar.add": "Add calendar",
517
+ // ---- desktop: calendar grids and agenda ----
518
+ "month.more": "+{n} more",
519
+ "grid.allDay": "all-day",
520
+ "grid.collapseAllDay": "Collapse all-day events",
521
+ "grid.showMoreAllDay": { one: "Show {n} more all-day event", other: "Show {n} more all-day events" },
522
+ "dur.m": "{m}m",
523
+ "dur.h": "{h}h",
524
+ "dur.hm": "{h}h {m}m",
525
+ "agenda.ended": "ended {time}",
526
+ "agenda.endsIn": "now \xB7 ends in {n}m",
527
+ "agenda.starts": "starts {when}",
528
+ "agenda.nothing": "Nothing scheduled today.",
529
+ "agenda.allDone": "All done for today.",
530
+ "agenda.now": "\u25CF now",
531
+ "agenda.joinShort": "Join",
532
+ "agenda.noCalls": "No calls today. Enjoy the quiet.",
533
+ "agenda.fact.calendar": "calendar",
534
+ "agenda.fact.where": "where",
535
+ "agenda.fact.people": "people",
536
+ "agenda.fact.link": "link",
537
+ "agenda.people": { one: "{n} person", other: "{n} people" },
538
+ "agenda.join": "Join call",
539
+ "agenda.joinOpens": "Join opens {n} min before start",
540
+ "agenda.details": "Details",
541
+ "agenda.otherDay": "{date} \xB7 not today",
542
+ "agenda.backToday": "Back to today",
543
+ "agenda.nothingDay": "Nothing scheduled this day.",
544
+ "agenda.pickHint": "Pick an event to see it here.",
545
+ "agenda.showPeople": "Show people",
546
+ "agenda.break": "Break \xB7 {d}",
547
+ "agenda.showAhead": "Next {n} days",
548
+ "agenda.hideAhead": "Hide upcoming",
549
+ "agenda.nothingAhead": "Nothing in the next {n} days.",
550
+ "agenda.hidePeople": "Hide people"
551
+ };
552
+
553
+ // ../../packages/core/src/i18n/uk.ts
554
+ var uk = {
555
+ // ---- core: shared bits ----
556
+ "common.untitled": "\u0411\u0435\u0437 \u043D\u0430\u0437\u0432\u0438",
557
+ "common.newEvent": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F",
558
+ "common.today": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
559
+ "when.allDay": "\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
560
+ "status.inMinutes": "\u0437\u0430 {n} \u0445\u0432",
561
+ "status.at": "\u043E {time}",
562
+ "meeting.videoCall": "\u0432\u0456\u0434\u0435\u043E\u0434\u0437\u0432\u0456\u043D\u043E\u043A",
563
+ "link.meet": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Google Meet",
564
+ "link.zoom": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Zoom",
565
+ "link.teams": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E Teams",
566
+ "link.video": "\u0414\u0437\u0432\u0456\u043D\u043E\u043A \xB7 {host}",
567
+ "link.map": "\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u0440\u0442\u0443",
568
+ // ---- core: repeat rules ----
569
+ "weekday.short.MO": "\u043F\u043D",
570
+ "weekday.short.TU": "\u0432\u0442",
571
+ "weekday.short.WE": "\u0441\u0440",
572
+ "weekday.short.TH": "\u0447\u0442",
573
+ "weekday.short.FR": "\u043F\u0442",
574
+ "weekday.short.SA": "\u0441\u0431",
575
+ "weekday.short.SU": "\u043D\u0434",
576
+ "weekday.on.MO": "\u0443 \u043F\u043E\u043D\u0435\u0434\u0456\u043B\u043E\u043A",
577
+ "weekday.on.TU": "\u0443 \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A",
578
+ "weekday.on.WE": "\u0443 \u0441\u0435\u0440\u0435\u0434\u0443",
579
+ "weekday.on.TH": "\u0443 \u0447\u0435\u0442\u0432\u0435\u0440",
580
+ "weekday.on.FR": "\u0443 \u043F\u02BC\u044F\u0442\u043D\u0438\u0446\u044E",
581
+ "weekday.on.SA": "\u0443 \u0441\u0443\u0431\u043E\u0442\u0443",
582
+ "weekday.on.SU": "\u0443 \u043D\u0435\u0434\u0456\u043B\u044E",
583
+ "repeat.none": "\u041D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044E\u0454\u0442\u044C\u0441\u044F",
584
+ "repeat.every.daily": "\u0429\u043E\u0434\u043D\u044F",
585
+ "repeat.every.weekly": "\u0429\u043E\u0442\u0438\u0436\u043D\u044F",
586
+ "repeat.every.monthly": "\u0429\u043E\u043C\u0456\u0441\u044F\u0446\u044F",
587
+ "repeat.every.yearly": "\u0429\u043E\u0440\u043E\u043A\u0443",
588
+ "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" },
589
+ "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" },
590
+ "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" },
591
+ "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" },
592
+ "repeat.weeklyOn": "\u0429\u043E\u0442\u0438\u0436\u043D\u044F {day}",
593
+ "repeat.monthlyOn": "\u0429\u043E\u043C\u0456\u0441\u044F\u0446\u044F {d}-\u0433\u043E \u0447\u0438\u0441\u043B\u0430",
594
+ "repeat.yearlyOn": "\u0429\u043E\u0440\u043E\u043A\u0443 {date}",
595
+ "repeat.weekdays": "\u0423 \u0431\u0443\u0434\u043D\u0456 (\u043F\u043D\u2013\u043F\u0442)",
596
+ "repeat.onDays": "{every} \u0443 {days}",
597
+ "repeat.onDay": "{every} {d}-\u0433\u043E \u0447\u0438\u0441\u043B\u0430",
598
+ "repeat.onDate": "{every} {date}",
599
+ "repeat.until": "\u0434\u043E {date}",
600
+ "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" },
601
+ "repeat.custom": "\u0412\u043B\u0430\u0441\u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E ({rule})",
602
+ // ---- core: event form errors ----
603
+ "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",
604
+ "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",
605
+ "form.invalidEmail": "\u041D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u0430 \u0430\u0434\u0440\u0435\u0441\u0430: {list}",
606
+ "form.chooseAccount": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
607
+ "form.chooseCalendar": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
608
+ // ---- core: notifications ----
609
+ "notify.invite": "\u041D\u043E\u0432\u0435 \u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
610
+ "notify.changed": "\u041F\u043E\u0434\u0456\u044E \u0437\u043C\u0456\u043D\u0435\u043D\u043E",
611
+ "notify.cancelled": "\u041F\u043E\u0434\u0456\u044E \u0441\u043A\u0430\u0441\u043E\u0432\u0430\u043D\u043E",
612
+ "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" },
613
+ // ---- desktop: menus, dialogs, update ----
614
+ "menu.file": "\u0424\u0430\u0439\u043B",
615
+ "menu.newEvent": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F",
616
+ "menu.close": "\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u0432\u0456\u043A\u043D\u043E",
617
+ "menu.quit": "\u0412\u0438\u0439\u0442\u0438",
618
+ "menu.edit": "\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u043D\u043D\u044F",
619
+ "menu.undo": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
620
+ "menu.redo": "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438",
621
+ "menu.cut": "\u0412\u0438\u0440\u0456\u0437\u0430\u0442\u0438",
622
+ "menu.copy": "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438",
623
+ "menu.paste": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u0438",
624
+ "menu.selectAll": "\u0412\u0438\u0434\u0456\u043B\u0438\u0442\u0438 \u0432\u0441\u0435",
625
+ "menu.view": "\u0412\u0438\u0433\u043B\u044F\u0434",
626
+ "menu.today": "\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
627
+ "menu.toggleSidebar": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438/\u0441\u0445\u043E\u0432\u0430\u0442\u0438 \u0431\u0456\u0447\u043D\u0443 \u043F\u0430\u043D\u0435\u043B\u044C",
628
+ "toolbar.hideSidebar": "\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u0431\u0456\u0447\u043D\u0443 \u043F\u0430\u043D\u0435\u043B\u044C",
629
+ "toolbar.showSidebar": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0431\u0456\u0447\u043D\u0443 \u043F\u0430\u043D\u0435\u043B\u044C",
630
+ "menu.reload": "\u041F\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438",
631
+ "menu.devTools": "\u0406\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0438 \u0440\u043E\u0437\u0440\u043E\u0431\u043D\u0438\u043A\u0430",
632
+ "menu.fullScreen": "\u041F\u043E\u0432\u043D\u043E\u0435\u043A\u0440\u0430\u043D\u043D\u0438\u0439 \u0440\u0435\u0436\u0438\u043C",
633
+ "menu.window": "\u0412\u0456\u043A\u043D\u043E",
634
+ "menu.minimize": "\u0417\u0433\u043E\u0440\u043D\u0443\u0442\u0438",
635
+ "menu.zoom": "\u041C\u0430\u0441\u0448\u0442\u0430\u0431\u0443\u0432\u0430\u0442\u0438",
636
+ "menu.front": "\u0423\u0441\u0456 \u043D\u0430 \u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043F\u043B\u0430\u043D",
637
+ "menu.openLink": "\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
638
+ "menu.copyLink": "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
639
+ "view.agenda": "\u0420\u043E\u0437\u043A\u043B\u0430\u0434",
640
+ "view.day": "\u0414\u0435\u043D\u044C",
641
+ "view.3day": "3 \u0434\u043D\u0456",
642
+ "view.week": "\u0422\u0438\u0436\u0434\u0435\u043D\u044C",
643
+ "view.month": "\u041C\u0456\u0441\u044F\u0446\u044C",
644
+ "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",
645
+ "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.",
646
+ "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",
647
+ "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.",
648
+ "update.available": "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u0432\u0435\u0440\u0441\u0456\u044F Mysticals {version}",
649
+ "update.clickToUpdate": "\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C, \u0449\u043E\u0431 \u043E\u043D\u043E\u0432\u0438\u0442\u0438",
650
+ "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",
651
+ // ---- desktop: app shell and toolbar ----
652
+ "common.undo": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
653
+ "common.cancel": "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
654
+ "common.close": "\u0417\u0430\u043A\u0440\u0438\u0442\u0438",
655
+ "common.save": "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438",
656
+ "common.delete": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",
657
+ "common.edit": "\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438",
658
+ "app.loading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F",
659
+ "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",
660
+ "toast.moved": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB",
661
+ "toast.movedAll": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB (\u0443\u0441\u0456 \u043F\u043E\u0434\u0456\u0457)",
662
+ "toast.movedFollowing": "\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043E \xAB{title}\xBB \u0456 \u043D\u0430\u0441\u0442\u0443\u043F\u043D\u0456",
663
+ "toast.moveFailed": "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438 \xAB{title}\xBB: {error}",
664
+ "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",
665
+ "toolbar.weekNo": "\u0422{n}",
666
+ "toolbar.previous": "\u041D\u0430\u0437\u0430\u0434",
667
+ "toolbar.next": "\u0412\u043F\u0435\u0440\u0435\u0434",
668
+ "toolbar.today": "\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
669
+ "toolbar.newEventHint": "\u041D\u043E\u0432\u0430 \u043F\u043E\u0434\u0456\u044F (N)",
670
+ "toolbar.new": "+ \u043D\u043E\u0432\u0430",
671
+ "toolbar.syncing": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
672
+ "toolbar.firstSync": "\u043F\u0435\u0440\u0448\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F: {names}\u2026",
673
+ // ---- desktop: settings ----
674
+ "settings.title": "\u041D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u043D\u044F",
675
+ "settings.sections": "\u0420\u043E\u0437\u0434\u0456\u043B\u0438 \u043D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u044C",
676
+ "settings.tab.accounts": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0456 \u0437\u0430\u043F\u0438\u0441\u0438",
677
+ "settings.tab.themes": "\u0422\u0435\u043C\u0438",
678
+ "settings.tab.language": "\u041C\u043E\u0432\u0430",
679
+ "settings.tab.sync": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
680
+ "settings.tab.privacy": "\u041A\u043E\u043D\u0444\u0456\u0434\u0435\u043D\u0446\u0456\u0439\u043D\u0456\u0441\u0442\u044C",
681
+ "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.",
682
+ "settings.addAccount": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441\u2026",
683
+ "settings.theme": "\u0422\u0435\u043C\u0430",
684
+ "theme.amber": "\u0411\u0443\u0440\u0448\u0442\u0438\u043D\u043E\u0432\u0438\u0439 \u043B\u044E\u043C\u0456\u043D\u043E\u0444\u043E\u0440",
685
+ "theme.mono": "\u0427\u043E\u0440\u043D\u043E-\u0431\u0456\u043B\u0430",
686
+ "theme.toon": "\u041C\u0443\u043B\u044C\u0442\u044F\u0448\u043D\u0430",
687
+ "settings.language.auto": "\u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E",
688
+ "settings.language.system": "\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u0430: {name}",
689
+ "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.",
690
+ "settings.sync.syncing": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F\u2026",
691
+ "settings.sync.all": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0441\u0435",
692
+ "settings.sync.now": "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0443\u0432\u0430\u0442\u0438",
693
+ "settings.sync.stateSyncing": "\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F",
694
+ "settings.sync.stateError": "\u043F\u043E\u043C\u0438\u043B\u043A\u0430",
695
+ "settings.sync.stateOk": "\u043E\u043A",
696
+ "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}",
697
+ "settings.reconnect": "\u041F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438 \u0437\u043D\u043E\u0432\u0443",
698
+ "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.",
699
+ "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",
700
+ "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",
701
+ "settings.account.labelFor": "\u041D\u0430\u0437\u0432\u0430 \u0434\u043B\u044F {email}",
702
+ "settings.account.colourFor": "\u041A\u043E\u043B\u0456\u0440 \u0434\u043B\u044F {email}",
703
+ "settings.account.remove": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",
704
+ "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.",
705
+ "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",
706
+ // ---- desktop: accounts ----
707
+ "common.back": "\u041D\u0430\u0437\u0430\u0434",
708
+ "common.decrease": "\u0417\u043C\u0435\u043D\u0448\u0438\u0442\u0438",
709
+ "common.increase": "\u0417\u0431\u0456\u043B\u044C\u0448\u0438\u0442\u0438",
710
+ "accounts.personal": "\u041E\u0441\u043E\u0431\u0438\u0441\u0442\u0438\u0439",
711
+ "accounts.work": "\u0420\u043E\u0431\u043E\u0447\u0438\u0439",
712
+ "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",
713
+ "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.",
714
+ "accounts.google.hint": "\u0412\u0445\u0456\u0434 \u0447\u0435\u0440\u0435\u0437 \u0431\u0440\u0430\u0443\u0437\u0435\u0440",
715
+ "accounts.caldav.hint": "Private Email, iCloud, Fastmail\u2026",
716
+ "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",
717
+ "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",
718
+ "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.",
719
+ "accounts.step.signedInGoogle": "\u0412\u0445\u0456\u0434 \u0443 Google \u0432\u0438\u043A\u043E\u043D\u0430\u043D\u043E",
720
+ "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",
721
+ "accounts.step.loadingCalendars": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0443\u0454\u043C\u043E \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u0456",
722
+ "accounts.step.signingIn": "\u0412\u0445\u0456\u0434 \u043D\u0430 {host}\u2026",
723
+ "accounts.field.provider": "\u0421\u0435\u0440\u0432\u0456\u0441",
724
+ "accounts.provider.custom": "\u0406\u043D\u0448\u0438\u0439",
725
+ "accounts.field.server": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430",
726
+ "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.",
727
+ "accounts.field.email": "\u0415\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0430 \u043F\u043E\u0448\u0442\u0430",
728
+ "accounts.field.password": "\u041F\u0430\u0440\u043E\u043B\u044C \u0437\u0430\u0441\u0442\u043E\u0441\u0443\u043D\u043A\u0443",
729
+ "accounts.field.label": "\u041D\u0430\u0437\u0432\u0430",
730
+ "accounts.field.colour": "\u041A\u043E\u043B\u0456\u0440",
731
+ "accounts.field.accountColour": "\u041A\u043E\u043B\u0456\u0440 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0441\u0443",
732
+ "accounts.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
733
+ // ---- desktop: event editor ----
734
+ "editor.title": "\u041D\u0430\u0437\u0432\u0430",
735
+ "editor.account": "\u041E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441",
736
+ "editor.chooseAccount": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043E\u0431\u043B\u0456\u043A\u043E\u0432\u0438\u0439 \u0437\u0430\u043F\u0438\u0441\u2026",
737
+ "editor.calendar": "\u041A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
738
+ "editor.chooseCalendar": "\u041E\u0431\u0435\u0440\u0456\u0442\u044C \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440\u2026",
739
+ "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",
740
+ "editor.allDay": "\u0423\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
741
+ "editor.starts": "\u041F\u043E\u0447\u0430\u0442\u043E\u043A",
742
+ "editor.ends": "\u041A\u0456\u043D\u0435\u0446\u044C",
743
+ "editor.location": "\u041C\u0456\u0441\u0446\u0435",
744
+ "editor.addLocation": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043C\u0456\u0441\u0446\u0435",
745
+ "editor.invitees": "\u0423\u0447\u0430\u0441\u043D\u0438\u043A\u0438",
746
+ "editor.removeInvitee": "\u041F\u0440\u0438\u0431\u0440\u0430\u0442\u0438 {email}",
747
+ "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)",
748
+ "editor.notes": "\u041D\u043E\u0442\u0430\u0442\u043A\u0438",
749
+ "editor.addNotes": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043D\u043E\u0442\u0430\u0442\u043A\u0438",
750
+ "editor.createdIn": "\u0421\u0442\u0432\u043E\u0440\u044E\u0454\u0442\u044C\u0441\u044F \u0432",
751
+ "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",
752
+ "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.",
753
+ "editor.saving": "\u0417\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043D\u044F\u2026",
754
+ "editor.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043F\u043E\u0434\u0456\u044E",
755
+ "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:",
756
+ "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",
757
+ // ---- desktop: repeat picker ----
758
+ "scope.one": "\u0426\u044F \u043F\u043E\u0434\u0456\u044F",
759
+ "scope.following": "\u0426\u044F \u0439 \u043D\u0430\u0441\u0442\u0443\u043F\u043D\u0456",
760
+ "scope.all": "\u0423\u0441\u0456 \u043F\u043E\u0434\u0456\u0457",
761
+ "repeat.label": "\u041F\u043E\u0432\u0442\u043E\u0440",
762
+ "repeat.loading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F\u2026",
763
+ "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.",
764
+ "repeat.customOption": "\u0406\u043D\u0448\u0435\u2026",
765
+ "repeat.every": "\u0420\u0430\u0437 \u043D\u0430",
766
+ "repeat.everyAria": "\u041F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u0442\u0438 \u043A\u043E\u0436\u043D\u0456",
767
+ "repeat.unitAria": "\u041E\u0434\u0438\u043D\u0438\u0446\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u0443",
768
+ "repeat.unit.daily": { one: "\u0434\u0435\u043D\u044C", few: "\u0434\u043D\u0456", many: "\u0434\u043D\u0456\u0432", other: "\u0434\u043D\u044F" },
769
+ "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" },
770
+ "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" },
771
+ "repeat.unit.yearly": { one: "\u0440\u0456\u043A", few: "\u0440\u043E\u043A\u0438", many: "\u0440\u043E\u043A\u0456\u0432", other: "\u0440\u043E\u043A\u0443" },
772
+ "repeat.on": "\u0414\u043D\u0456",
773
+ "repeat.onAria": "\u041F\u043E\u0432\u0442\u043E\u0440\u044E\u0432\u0430\u0442\u0438 \u0432 \u0434\u043D\u0456",
774
+ "repeat.stops": "\u0417\u0430\u043A\u0456\u043D\u0447\u0435\u043D\u043D\u044F",
775
+ "repeat.stopsAria": "\u041A\u043E\u043B\u0438 \u0437\u0430\u043A\u0456\u043D\u0447\u0438\u0442\u0438 \u043F\u043E\u0432\u0442\u043E\u0440",
776
+ "repeat.never": "\u041D\u0456\u043A\u043E\u043B\u0438",
777
+ "repeat.endOnDate": "\u0414\u0430\u0442\u0430",
778
+ "repeat.after": "\u041F\u0456\u0441\u043B\u044F",
779
+ "repeat.stopsOn": "\u0417\u0430\u043A\u0456\u043D\u0447\u0443\u0454\u0442\u044C\u0441\u044F",
780
+ "repeat.occurrences": "\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0456\u0432",
781
+ "repeat.times": { one: "\u0440\u0430\u0437", few: "\u0440\u0430\u0437\u0438", many: "\u0440\u0430\u0437\u0456\u0432", other: "\u0440\u0430\u0437\u0443" },
782
+ // ---- desktop: date picker and event details ----
783
+ "dtf.date": "{label}: \u0434\u0430\u0442\u0430",
784
+ "dtf.time": "{label}: \u0447\u0430\u0441",
785
+ "dtf.typed": "{label}: \u0447\u0430\u0441, \u0413\u0413:\u0425\u0425",
786
+ "rsvp.accept": "\u041F\u0440\u0438\u0439\u043D\u044F\u0442\u0438",
787
+ "rsvp.maybe": "\u041C\u043E\u0436\u043B\u0438\u0432\u043E",
788
+ "rsvp.decline": "\u0412\u0456\u0434\u0445\u0438\u043B\u0438\u0442\u0438",
789
+ "rsvp.reply": "\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C",
790
+ "rsvp.replyAs": "\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u0432\u0456\u0434 {email}",
791
+ "partstat.accepted": "\u043F\u0440\u0438\u0439\u043D\u044F\u0442\u043E",
792
+ "partstat.tentative": "\u043C\u043E\u0436\u043B\u0438\u0432\u043E",
793
+ "partstat.declined": "\u0432\u0456\u0434\u0445\u0438\u043B\u0435\u043D\u043E",
794
+ "partstat.needsAction": "\u0431\u0435\u0437 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u0456",
795
+ "details.deleted": "\u0426\u044E \u043F\u043E\u0434\u0456\u044E \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u043E",
796
+ "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.",
797
+ "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.",
798
+ "details.calendarIn": "{calendar} \u0443",
799
+ "details.organizer": "\u041E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440",
800
+ "details.invitees": "\u0423\u0447\u0430\u0441\u043D\u0438\u043A\u0438 ({n})",
801
+ "details.you": "\u0432\u0438",
802
+ "details.organizerTag": "\u043E\u0440\u0433\u0430\u043D\u0456\u0437\u0430\u0442\u043E\u0440",
803
+ "details.confirmDelete": "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0446\u044E \u043F\u043E\u0434\u0456\u044E?",
804
+ "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",
805
+ // ---- desktop: invites, mini month, reconnect ----
806
+ "invites.title": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
807
+ "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",
808
+ "invites.aria": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F ({n})",
809
+ "invites.hint": "\u0417\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F (i)",
810
+ "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" },
811
+ "mini.prev": "\u041F\u043E\u043F\u0435\u0440\u0435\u0434\u043D\u0456\u0439 \u043C\u0456\u0441\u044F\u0446\u044C",
812
+ "mini.next": "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0438\u0439 \u043C\u0456\u0441\u044F\u0446\u044C",
813
+ "reauth.done": "{label}: \u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0437\u043D\u043E\u0432\u0443",
814
+ "reauth.title": "{label}: \u043F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0438\u0439 \u0432\u0445\u0456\u0434",
815
+ "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.",
816
+ "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.",
817
+ "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.",
818
+ "reauth.signingIn": "\u0412\u0445\u0456\u0434\u2026",
819
+ "reauth.later": "\u041F\u0456\u0437\u043D\u0456\u0448\u0435",
820
+ "reauth.signInAgain": "\u0423\u0432\u0456\u0439\u0442\u0438 \u0437\u043D\u043E\u0432\u0443",
821
+ // ---- desktop: status bar and sidebar ----
822
+ "sbar.until": "\u0434\u043E {time}",
823
+ "sbar.next": "\u0434\u0430\u043B\u0456:",
824
+ "sbar.noUpcoming": "\u043D\u0430\u0439\u0431\u043B\u0438\u0436\u0447\u0438\u0445 \u043F\u043E\u0434\u0456\u0439 \u043D\u0435\u043C\u0430\u0454",
825
+ "sbar.updateTo": "\u2191 \u041E\u043D\u043E\u0432\u0438\u0442\u0438 \u0434\u043E {version}",
826
+ "sbar.download": "\u2193 \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 {version}",
827
+ "sbar.downloading": "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F {n}%",
828
+ "sbar.restarting": "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u2026",
829
+ "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",
830
+ "sbar.mode.agenda": "\u0440\u043E\u0437\u043A\u043B\u0430\u0434",
831
+ "sbar.mode.day": "\u0434\u0435\u043D\u044C",
832
+ "sbar.mode.3day": "3 \u0434\u043D\u0456",
833
+ "sbar.mode.week": "\u0442\u0438\u0436\u0434\u0435\u043D\u044C",
834
+ "sbar.mode.month": "\u043C\u0456\u0441\u044F\u0446\u044C",
835
+ "sbar.syncing": "\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u044F: {names}",
836
+ "sbar.disconnected": "\u2715 \u043F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u0432\u0445\u0456\u0434",
837
+ "sbar.syncError": "\u2715 \u043F\u043E\u043C\u0438\u043B\u043A\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u0457",
838
+ "sbar.key.new": "\u043D\u043E\u0432\u0430",
839
+ "sbar.key.today": "\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
840
+ "sbar.key.invites": "\u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u043D\u044F",
841
+ "sidebar.syncError": "\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0456\u0437\u0430\u0446\u0456\u0457: {error}",
842
+ "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",
843
+ "sidebar.disconnected": "\u041F\u043E\u0442\u0440\u0456\u0431\u0435\u043D \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0438\u0439 \u0432\u0445\u0456\u0434",
844
+ "sidebar.reconnect": "\u043F\u0456\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0438",
845
+ "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",
846
+ "sidebar.retry": "\u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438",
847
+ "sidebar.readOnlyTitle": "\u041B\u0438\u0448\u0435 \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u0434",
848
+ "sidebar.readOnly": "\u043B\u0438\u0448\u0435 \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u0434",
849
+ "sidebar.add": "\u0414\u043E\u0434\u0430\u0442\u0438 \u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
850
+ // ---- desktop: calendar grids and agenda ----
851
+ "month.more": "\u0449\u0435 {n}",
852
+ "grid.allDay": "\u0432\u0435\u0441\u044C \u0434\u0435\u043D\u044C",
853
+ "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",
854
+ "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" },
855
+ "dur.m": "{m} \u0445\u0432",
856
+ "dur.h": "{h} \u0433\u043E\u0434",
857
+ "dur.hm": "{h} \u0433\u043E\u0434 {m} \u0445\u0432",
858
+ "agenda.ended": "\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044F \u043E {time}",
859
+ "agenda.endsIn": "\u0437\u0430\u0440\u0430\u0437 \xB7 \u0434\u043E \u043A\u0456\u043D\u0446\u044F {n} \u0445\u0432",
860
+ "agenda.starts": "\u043F\u043E\u0447\u0438\u043D\u0430\u0454\u0442\u044C\u0441\u044F {when}",
861
+ "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.",
862
+ "agenda.allDone": "\u041D\u0430 \u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456 \u0432\u0441\u0435.",
863
+ "agenda.now": "\u25CF \u0437\u0430\u0440\u0430\u0437",
864
+ "agenda.joinShort": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F",
865
+ "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.",
866
+ "agenda.fact.calendar": "\u043A\u0430\u043B\u0435\u043D\u0434\u0430\u0440",
867
+ "agenda.fact.where": "\u0434\u0435",
868
+ "agenda.fact.people": "\u0443\u0447\u0430\u0441\u043D\u0438\u043A\u0438",
869
+ "agenda.fact.link": "\u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
870
+ "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" },
871
+ "agenda.join": "\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F \u0434\u043E \u0434\u0437\u0432\u0456\u043D\u043A\u0430",
872
+ "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",
873
+ "agenda.details": "\u0414\u043E\u043A\u043B\u0430\u0434\u043D\u0456\u0448\u0435",
874
+ "agenda.otherDay": "{date} \xB7 \u043D\u0435 \u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
875
+ "agenda.backToday": "\u041F\u043E\u0432\u0435\u0440\u043D\u0443\u0442\u0438\u0441\u044C \u043D\u0430 \u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456",
876
+ "agenda.nothingDay": "\u0426\u044C\u043E\u0433\u043E \u0434\u043D\u044F \u043D\u0456\u0447\u043E\u0433\u043E \u043D\u0435 \u0437\u0430\u043F\u043B\u0430\u043D\u043E\u0432\u0430\u043D\u043E.",
877
+ "agenda.pickHint": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044C \u043F\u043E\u0434\u0456\u044E, \u0449\u043E\u0431 \u043F\u043E\u0431\u0430\u0447\u0438\u0442\u0438 \u0457\u0457 \u0442\u0443\u0442.",
878
+ "agenda.showPeople": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0443\u0447\u0430\u0441\u043D\u0438\u043A\u0456\u0432",
879
+ "agenda.break": "\u041F\u0435\u0440\u0435\u0440\u0432\u0430 \xB7 {d}",
880
+ "agenda.showAhead": "\u041D\u0430\u0441\u0442\u0443\u043F\u043D\u0456 {n} \u0434\u043D\u0456\u0432",
881
+ "agenda.hideAhead": "\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u043D\u0430\u0441\u0442\u0443\u043F\u043D\u0456",
882
+ "agenda.nothingAhead": "\u041D\u0430\u0439\u0431\u043B\u0438\u0436\u0447\u0456 {n} \u0434\u043D\u0456\u0432 \u043D\u0456\u0447\u043E\u0433\u043E \u043D\u0435\u043C\u0430\u0454.",
883
+ "agenda.hidePeople": "\u0421\u0445\u043E\u0432\u0430\u0442\u0438 \u0443\u0447\u0430\u0441\u043D\u0438\u043A\u0456\u0432"
884
+ };
885
+
886
+ // ../../packages/core/src/i18n.ts
887
+ var DICT = { en, uk };
888
+ var plurals = /* @__PURE__ */ new Map();
889
+ var pluralOf = (locale, n) => {
890
+ if (!plurals.has(locale)) plurals.set(locale, new Intl.PluralRules(locale));
891
+ return plurals.get(locale).select(n);
892
+ };
893
+ function t(locale, key3, params) {
894
+ const msg = DICT[locale][key3] ?? en[key3];
895
+ const text2 = typeof msg === "string" ? msg : msg[pluralOf(locale, Number(params?.n ?? 0))] ?? msg.other;
896
+ return params ? text2.replace(/\{(\w+)\}/g, (m, k) => k in params ? String(params[k]) : m) : text2;
897
+ }
898
+ var dateLocale = (locale) => locale === "uk" ? ukDates : enUS;
899
+ var fmt = (locale, date, pattern) => format(date, locale === "uk" ? pattern.replace(/(?<!E)EEE(?!E)/g, "EEEEEE") : pattern, { locale: dateLocale(locale) });
900
+
901
+ // ../../packages/core/src/sync/notify.ts
214
902
  var at = (iso2, allDay) => (allDay ? parseISO(iso2) : new Date(iso2)).getTime();
215
903
  var key = (e) => `${e.calendarId}/${e.id}`;
216
904
  var WATCHED = ["title", "start", "end", "location"];
@@ -229,15 +917,18 @@ function diffEvents(prev, next, now = /* @__PURE__ */ new Date()) {
229
917
  for (const e of prev) if (live(e) && !after.has(key(e))) notes.push({ kind: "cancelled", event: e });
230
918
  return notes;
231
919
  }
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}` }));
920
+ var TITLE = { invite: "notify.invite", changed: "notify.changed", cancelled: "notify.cancelled" };
921
+ function noteText(notes, account, now = /* @__PURE__ */ new Date(), locale = "en") {
922
+ if (notes.length > 3) return [{ title: t(locale, "notify.many", { n: notes.length }), body: account }];
923
+ return notes.map(({ kind, event: e }) => ({
924
+ title: `${t(locale, TITLE[kind])}: ${e.title || t(locale, "common.untitled")}`,
925
+ body: `${when(e, now, locale)} \xB7 ${account}`
926
+ }));
236
927
  }
237
- function when(e, now) {
928
+ function when(e, now, locale) {
238
929
  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")}`;
930
+ if (e.allDay) return `${fmt(locale, s, "EEE, d MMM")} \xB7 ${t(locale, "when.allDay")}`;
931
+ return `${isSameDay(s, now) ? t(locale, "common.today") : fmt(locale, s, "EEE, d MMM")} \xB7 ${format2(s, "HH:mm")}`;
241
932
  }
242
933
 
243
934
  // ../../packages/core/src/providers/http.ts
@@ -292,7 +983,7 @@ var SyncEngine = class {
292
983
  this.running = false;
293
984
  this.unsubscribe?.();
294
985
  this.unsubscribe = void 0;
295
- for (const t of this.timers.values()) clearTimeout(t);
986
+ for (const t2 of this.timers.values()) clearTimeout(t2);
296
987
  this.timers.clear();
297
988
  }
298
989
  /**
@@ -465,7 +1156,7 @@ function startTelemetry(o) {
465
1156
  } catch {
466
1157
  return void 0;
467
1158
  }
468
- const host = env.MYSTICALS_TELEMETRY_HOST || HOST;
1159
+ const host2 = env.MYSTICALS_TELEMETRY_HOST || HOST;
469
1160
  const doFetch = o.fetch ?? fetch;
470
1161
  const track = (event, props) => {
471
1162
  if (o.allowed && !o.allowed()) return;
@@ -479,7 +1170,7 @@ function startTelemetry(o) {
479
1170
  $geoip_disable: true
480
1171
  };
481
1172
  try {
482
- doFetch(`${host}/i/v0/e/`, {
1173
+ doFetch(`${host2}/i/v0/e/`, {
483
1174
  method: "POST",
484
1175
  headers: { "Content-Type": "application/json" },
485
1176
  body: JSON.stringify({ api_key: o.key, event, distinct_id: install.id, properties }),
@@ -494,89 +1185,626 @@ function startTelemetry(o) {
494
1185
  return { track, installed: install.fresh };
495
1186
  }
496
1187
 
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);
1188
+ // ../../packages/core/src/logic/recurrence.ts
1189
+ import { format as format3, isValid, parseISO as parseISO3 } from "date-fns";
1190
+ var WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
1191
+ var JS_DAY = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
1192
+ var WORKWEEK = ["MO", "TU", "WE", "TH", "FR"];
1193
+ var FREQS = ["daily", "weekly", "monthly", "yearly"];
1194
+ var UNIT = { daily: "day", weekly: "week", monthly: "month", yearly: "year" };
1195
+ var asDate = (start) => start.length === 10 ? parseISO3(start) : new Date(start);
1196
+ var weekdayOf = (start) => JS_DAY[asDate(start).getDay()];
1197
+ var shortDay = (d, locale = "en") => t(locale, `weekday.short.${d}`);
1198
+ function normalize(r, start) {
1199
+ if (r.rule) return { freq: r.freq, rule: r.rule };
1200
+ const out = { freq: r.freq };
1201
+ if (r.interval && r.interval > 1) out.interval = r.interval;
1202
+ if (r.freq === "weekly" && r.byDay?.length) {
1203
+ const days = WEEKDAYS.filter((d) => r.byDay.includes(d));
1204
+ if (!(days.length === 1 && days[0] === weekdayOf(start))) out.byDay = days;
1205
+ }
1206
+ if (r.until) out.until = r.until;
1207
+ else if (r.count) out.count = r.count;
1208
+ return out;
1209
+ }
1210
+ function sameRecurrence(a, b, start) {
1211
+ if (!a || !b) return a === b;
1212
+ return JSON.stringify(normalize(a, start)) === JSON.stringify(normalize(b, start));
1213
+ }
1214
+ function toRRule(r, allDay) {
1215
+ if (r.rule) return r.rule;
1216
+ const parts = [`FREQ=${r.freq.toUpperCase()}`];
1217
+ if (r.interval && r.interval > 1) parts.push(`INTERVAL=${r.interval}`);
1218
+ if (r.freq === "weekly" && r.byDay?.length) parts.push(`BYDAY=${WEEKDAYS.filter((d) => r.byDay.includes(d)).join(",")}`);
1219
+ if (r.until) {
1220
+ const until = allDay ? r.until.replace(/-/g, "") : (/* @__PURE__ */ new Date(`${r.until}T23:59:59`)).toISOString().replace(/[-:]|\.\d{3}/g, "");
1221
+ parts.push(`UNTIL=${until}`);
1222
+ } else if (r.count) parts.push(`COUNT=${r.count}`);
1223
+ return parts.join(";");
1224
+ }
1225
+ var EDITABLE = /* @__PURE__ */ new Set(["FREQ", "INTERVAL", "BYDAY", "UNTIL", "COUNT", "WKST"]);
1226
+ function fromRRule(text2, start) {
1227
+ const rule = text2.replace(/^RRULE:/i, "").trim();
1228
+ const kv = new Map(rule.split(";").filter(Boolean).map((p) => {
1229
+ const i = p.indexOf("=");
1230
+ return [p.slice(0, i).toUpperCase(), p.slice(i + 1).toUpperCase()];
1231
+ }));
1232
+ const freq = (kv.get("FREQ") ?? "").toLowerCase();
1233
+ const custom = () => ({ freq: FREQS.includes(freq) ? freq : "daily", rule });
1234
+ if (!FREQS.includes(freq) || [...kv.keys()].some((k) => !EDITABLE.has(k))) return custom();
1235
+ const out = { freq };
1236
+ const interval = Number(kv.get("INTERVAL") ?? 1);
1237
+ if (!Number.isInteger(interval) || interval < 1) return custom();
1238
+ if (interval > 1) out.interval = interval;
1239
+ if (kv.has("BYDAY")) {
1240
+ const days = kv.get("BYDAY").split(",");
1241
+ if (freq !== "weekly" || days.some((d) => !WEEKDAYS.includes(d))) return custom();
1242
+ out.byDay = WEEKDAYS.filter((d) => days.includes(d));
1243
+ }
1244
+ const until = kv.get("UNTIL");
1245
+ if (until) {
1246
+ const m = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/.exec(until);
1247
+ if (!m) return custom();
1248
+ if (!m[4]) out.until = `${m[1]}-${m[2]}-${m[3]}`;
1249
+ else {
1250
+ 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]);
1251
+ const s = start && start.length > 10 ? asDate(start) : void 0;
1252
+ const minutes = (d) => d.getHours() * 60 + d.getMinutes();
1253
+ if (s && minutes(at3) < minutes(s)) at3.setDate(at3.getDate() - 1);
1254
+ out.until = format3(at3, "yyyy-MM-dd");
577
1255
  }
1256
+ } else if (kv.has("COUNT")) {
1257
+ const count = Number(kv.get("COUNT"));
1258
+ if (!Number.isInteger(count) || count < 1) return custom();
1259
+ out.count = count;
1260
+ }
1261
+ return out;
1262
+ }
1263
+ function presets(start, locale = "en") {
1264
+ const d = asDate(start);
1265
+ return [
1266
+ { id: "none", label: t(locale, "repeat.none"), recurrence: null },
1267
+ { id: "daily", label: t(locale, "repeat.every.daily"), recurrence: { freq: "daily" } },
1268
+ { id: "weekly", label: t(locale, "repeat.weeklyOn", { day: t(locale, `weekday.on.${weekdayOf(start)}`) }), recurrence: { freq: "weekly" } },
1269
+ { id: "monthly", label: t(locale, "repeat.monthlyOn", { d: d.getDate() }), recurrence: { freq: "monthly" } },
1270
+ { id: "yearly", label: t(locale, "repeat.yearlyOn", { date: fmt(locale, d, "d MMMM") }), recurrence: { freq: "yearly" } },
1271
+ { id: "weekdays", label: t(locale, "repeat.weekdays"), recurrence: { freq: "weekly", byDay: WORKWEEK } }
1272
+ ];
1273
+ }
1274
+ function presetOf(r, start) {
1275
+ if (!r) return "none";
1276
+ if (r.rule) return "custom";
1277
+ const { until: _u, count: _c, ...base } = normalize(r, start);
1278
+ const hit = presets(start).find((p) => p.recurrence && JSON.stringify(normalize(p.recurrence, start)) === JSON.stringify(base));
1279
+ return hit?.id ?? "custom";
1280
+ }
1281
+ var withEnd = (r, from) => from?.until ? { ...r, until: from.until, count: void 0 } : from?.count ? { ...r, count: from.count, until: void 0 } : r;
1282
+ function describe(r, start, locale = "en") {
1283
+ if (!r) return t(locale, "repeat.none");
1284
+ if (r.rule) return t(locale, "repeat.custom", { rule: r.rule });
1285
+ const n = normalize(r, start);
1286
+ const d = asDate(start);
1287
+ const every = n.interval ? t(locale, `repeat.everyN.${n.freq}`, { n: n.interval }) : t(locale, `repeat.every.${n.freq}`);
1288
+ 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;
1289
+ 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 })}` : "";
1290
+ return head + end;
1291
+ }
1292
+ var everyText = (r) => `${r.interval ?? 1} ${UNIT[r.freq]}${(r.interval ?? 1) > 1 ? "s" : ""}`;
1293
+ function parseEvery(text2) {
1294
+ const m = /^\s*(\d+)?\s*(day|week|month|year)s?\s*$/i.exec(text2);
1295
+ const n = Number(m?.[1] ?? 1);
1296
+ if (!m || n < 1 || n > 999) throw new Error('Repeat every: use e.g. "2 weeks" (day, week, month or year)');
1297
+ const freq = FREQS.find((f) => UNIT[f] === m[2].toLowerCase());
1298
+ return { freq, interval: n > 1 ? n : void 0 };
1299
+ }
1300
+ var daysText = (days) => days.map((d) => d.toLowerCase()).join(" ");
1301
+ function parseDays(text2) {
1302
+ const words = text2.split(/[\s,;]+/).filter(Boolean).map((w) => w.slice(0, 2).toUpperCase());
1303
+ const bad = words.filter((w) => !WEEKDAYS.includes(w));
1304
+ if (!words.length || bad.length) throw new Error('Repeat on: use days like "mo th"');
1305
+ return WEEKDAYS.filter((d) => words.includes(d));
1306
+ }
1307
+ var endsText = (r) => r.until ? r.until : r.count ? `${r.count} times` : "never";
1308
+ function parseEnds(text2) {
1309
+ const t2 = text2.trim().toLowerCase();
1310
+ if (!t2 || t2 === "never") return {};
1311
+ if (/^\d{4}-\d{2}-\d{2}$/.test(t2) && isValid(parseISO3(t2)) && format3(parseISO3(t2), "yyyy-MM-dd") === t2) return { until: t2 };
1312
+ const m = /^(\d+)(\s*times?)?$/.exec(t2);
1313
+ if (m && +m[1] >= 1 && +m[1] <= 999) return { count: +m[1] };
1314
+ throw new Error('Ends: use "never", a date (YYYY-MM-DD) or "10 times"');
1315
+ }
1316
+
1317
+ // ../../packages/core/src/providers/google/oauth.ts
1318
+ import { createHash as createHash2, randomBytes } from "crypto";
1319
+ import { createServer } from "http";
1320
+ var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
1321
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
1322
+ var SCOPES = "openid email https://www.googleapis.com/auth/calendar.calendarlist.readonly https://www.googleapis.com/auth/calendar.events";
1323
+ var TIMEOUT_MS = 5 * 60 * 1e3;
1324
+ var clientConfig = {};
1325
+ function setClientConfig(cfg) {
1326
+ clientConfig = cfg;
1327
+ }
1328
+ function getClientConfig() {
1329
+ const { clientId, clientSecret } = clientConfig;
1330
+ 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");
1331
+ return { clientId, clientSecret };
1332
+ }
1333
+ function createPkce() {
1334
+ const verifier = randomBytes(32).toString("base64url");
1335
+ return { verifier, challenge: createHash2("sha256").update(verifier).digest("base64url") };
1336
+ }
1337
+ function buildAuthUrl(p) {
1338
+ const url = new URL(AUTH_URL);
1339
+ url.search = new URLSearchParams({
1340
+ client_id: p.clientId,
1341
+ redirect_uri: p.redirectUri,
1342
+ response_type: "code",
1343
+ scope: SCOPES,
1344
+ state: p.state,
1345
+ code_challenge: p.codeChallenge,
1346
+ code_challenge_method: "S256",
1347
+ access_type: "offline",
1348
+ prompt: "consent"
1349
+ }).toString();
1350
+ return url.toString();
1351
+ }
1352
+ function emailFromIdToken(idToken) {
1353
+ try {
1354
+ const payload = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString("utf8"));
1355
+ return typeof payload.email === "string" ? payload.email : void 0;
1356
+ } catch {
1357
+ return void 0;
1358
+ }
1359
+ }
1360
+ function parseTokenResponse(json, now = Date.now()) {
1361
+ const j = json ?? {};
1362
+ if (typeof j.error === "string") {
1363
+ const msg = `Google token error: ${j.error}${j.error_description ? ` (${j.error_description})` : ""}`;
1364
+ throw j.error === "invalid_grant" ? new AuthError(msg) : new Error(msg);
1365
+ }
1366
+ if (typeof j.access_token !== "string") throw new Error("Google token response has no access_token");
1367
+ const expiresIn = typeof j.expires_in === "number" ? j.expires_in : 3600;
1368
+ return {
1369
+ accessToken: j.access_token,
1370
+ expiresAt: now + expiresIn * 1e3,
1371
+ refreshToken: typeof j.refresh_token === "string" ? j.refresh_token : void 0,
1372
+ email: typeof j.id_token === "string" ? emailFromIdToken(j.id_token) : void 0
578
1373
  };
579
- const upsert = (ev, accountId, calendarId) => {
1374
+ }
1375
+ async function postToken(params) {
1376
+ const res = await timedFetch(TOKEN_URL, {
1377
+ method: "POST",
1378
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1379
+ body: new URLSearchParams(params).toString()
1380
+ });
1381
+ const json = await res.json().catch(() => ({ error: `http_${res.status}` }));
1382
+ if (!res.ok && !json.error) json.error = `http_${res.status}`;
1383
+ return parseTokenResponse(json);
1384
+ }
1385
+ 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>`);
1386
+ async function runOAuthFlow(openUrl3, opts = {}) {
1387
+ const cfg = getClientConfig();
1388
+ const { verifier, challenge } = createPkce();
1389
+ const state = randomBytes(16).toString("base64url");
1390
+ const server = createServer();
1391
+ try {
1392
+ await new Promise((resolve, reject) => {
1393
+ server.once("error", reject);
1394
+ server.listen(0, "127.0.0.1", resolve);
1395
+ });
1396
+ const redirectUri = `http://127.0.0.1:${server.address().port}`;
1397
+ const code = await new Promise((resolve, reject) => {
1398
+ const timer = setTimeout(() => reject(new Error("Google sign-in timed out")), TIMEOUT_MS);
1399
+ const finish = (err, value) => {
1400
+ clearTimeout(timer);
1401
+ if (err) reject(err);
1402
+ else resolve(value);
1403
+ };
1404
+ server.on("request", (req, res) => {
1405
+ const params = new URL(req.url ?? "/", redirectUri).searchParams;
1406
+ const reply = (status, msg, then) => {
1407
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", Connection: "close" });
1408
+ res.end(DONE_HTML(msg, status === 200 ? opts.returnUrl : void 0), then);
1409
+ };
1410
+ if (!params.has("code") && !params.has("error")) {
1411
+ res.writeHead(404, { Connection: "close" }).end();
1412
+ return;
1413
+ }
1414
+ if (params.get("state") !== state) {
1415
+ reply(400, "Sign-in failed", () => finish(new Error("Google sign-in failed: state mismatch")));
1416
+ } else if (params.has("error")) {
1417
+ reply(400, "Sign-in cancelled", () => finish(new Error(`Google sign-in failed: ${params.get("error")}`)));
1418
+ } else {
1419
+ const code2 = params.get("code");
1420
+ reply(200, "Signed in", () => finish(null, code2));
1421
+ }
1422
+ });
1423
+ openUrl3(buildAuthUrl({ clientId: cfg.clientId, redirectUri, state, codeChallenge: challenge })).catch(
1424
+ (e) => finish(e instanceof Error ? e : new Error(String(e)))
1425
+ );
1426
+ });
1427
+ opts.onCode?.();
1428
+ const tok = await postToken({
1429
+ grant_type: "authorization_code",
1430
+ code,
1431
+ code_verifier: verifier,
1432
+ redirect_uri: redirectUri,
1433
+ client_id: cfg.clientId,
1434
+ client_secret: cfg.clientSecret
1435
+ });
1436
+ if (!tok.refreshToken) throw new Error("Google did not return a refresh token");
1437
+ const email = tok.email ?? await fetchUserEmail(tok.accessToken);
1438
+ return { email, credentials: { kind: "google", refreshToken: tok.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt } };
1439
+ } finally {
1440
+ server.closeAllConnections();
1441
+ server.close();
1442
+ }
1443
+ }
1444
+ async function fetchUserEmail(accessToken) {
1445
+ const res = await timedFetch("https://openidconnect.googleapis.com/v1/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
1446
+ const json = await res.json().catch(() => ({}));
1447
+ if (!res.ok || typeof json.email !== "string") throw new Error("Could not read Google account email");
1448
+ return json.email;
1449
+ }
1450
+
1451
+ // ../../packages/core/src/providers/google/provider.ts
1452
+ var API = "https://www.googleapis.com/calendar/v3";
1453
+ function conferenceUrlOf(g) {
1454
+ const uri = g.hangoutLink ?? g.conferenceData?.entryPoints?.find((p) => p.entryPointType === "video")?.uri;
1455
+ return uri && /^https?:\/\//i.test(uri) ? uri : void 0;
1456
+ }
1457
+ var STATUSES = ["accepted", "declined", "tentative", "needsAction"];
1458
+ var toStatus = (s) => STATUSES.includes(s) ? s : "needsAction";
1459
+ function mapEvent(g, accountId, calendarId) {
1460
+ const allDay = !!g.start.date;
1461
+ const time = (t2) => allDay ? t2.date : new Date(t2.dateTime).toISOString();
1462
+ const attendees = (g.attendees ?? []).map((a) => ({
1463
+ email: a.email,
1464
+ name: a.displayName,
1465
+ status: toStatus(a.responseStatus),
1466
+ self: a.self || void 0,
1467
+ organizer: a.organizer || void 0
1468
+ }));
1469
+ const self = g.attendees?.find((a) => a.self);
1470
+ return {
1471
+ id: g.id,
1472
+ accountId,
1473
+ calendarId,
1474
+ title: g.summary ?? "",
1475
+ start: time(g.start),
1476
+ end: time(g.end),
1477
+ allDay,
1478
+ location: g.location,
1479
+ conferenceUrl: conferenceUrlOf(g),
1480
+ description: g.description,
1481
+ organizer: g.organizer?.email ? { email: g.organizer.email, name: g.organizer.displayName } : void 0,
1482
+ attendees,
1483
+ myStatus: self ? toStatus(self.responseStatus) : void 0,
1484
+ etag: g.etag,
1485
+ raw: g,
1486
+ recurringEventId: g.recurringEventId
1487
+ };
1488
+ }
1489
+ function truncateRecurrence(recurrence, cutoff) {
1490
+ 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, "");
1491
+ return recurrence.map((line) => {
1492
+ if (!line.startsWith("RRULE:")) return line;
1493
+ const parts = line.slice(6).split(";").filter((p) => !/^(COUNT|UNTIL)=/i.test(p));
1494
+ return `RRULE:${[...parts, `UNTIL=${until}`].join(";")}`;
1495
+ });
1496
+ }
1497
+ var DAY = 864e5;
1498
+ var plusDays = (date, days) => new Date(Date.parse(date.slice(0, 10)) + days * DAY).toISOString().slice(0, 10);
1499
+ var daysBetween = (a, b) => Math.round((Date.parse(b.slice(0, 10)) - Date.parse(a.slice(0, 10))) / DAY);
1500
+ var localDate = (d) => [d.getFullYear(), d.getMonth() + 1, d.getDate()].map((n) => String(n).padStart(2, "0")).join("-");
1501
+ function shiftAnchor(anchor, before, start, allDay, timeZone) {
1502
+ if (allDay) return { date: plusDays(anchor.date ?? anchor.dateTime, daysBetween(before.date ?? before.dateTime, start)) };
1503
+ if (anchor.dateTime && before.dateTime) {
1504
+ return { dateTime: new Date(Date.parse(anchor.dateTime) + Date.parse(start) - Date.parse(before.dateTime)).toISOString(), timeZone };
1505
+ }
1506
+ const at3 = new Date(start);
1507
+ const day = /* @__PURE__ */ new Date(`${plusDays(anchor.date, daysBetween(before.date, localDate(at3)))}T00:00:00`);
1508
+ day.setHours(at3.getHours(), at3.getMinutes(), at3.getSeconds());
1509
+ return { dateTime: day.toISOString(), timeZone };
1510
+ }
1511
+ function endFrom(from, event, timeZone) {
1512
+ if (event.allDay) return { date: plusDays(from.date, daysBetween(event.start, event.end)) };
1513
+ return { dateTime: new Date(Date.parse(from.dateTime) + Date.parse(event.end) - Date.parse(event.start)).toISOString(), timeZone };
1514
+ }
1515
+ var toGTime = (iso2, allDay) => allDay ? { date: iso2.slice(0, 10), dateTime: null, timeZone: null } : { dateTime: iso2, date: null, timeZone: null };
1516
+ var zoned = (iso2, allDay, timeZone) => allDay ? { date: iso2.slice(0, 10) } : { dateTime: iso2, timeZone };
1517
+ var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
1518
+ var rruleLines = (r, allDay) => [`RRULE:${toRRule(r, allDay)}`];
1519
+ var nulled = (t2) => ({ date: null, dateTime: null, timeZone: null, ...t2 });
1520
+ function createGoogleProviderImpl(ctx) {
1521
+ if (ctx.credentials.kind !== "google") throw new Error("Google provider needs Google credentials");
1522
+ let creds = ctx.credentials;
1523
+ let refreshing;
1524
+ const refresh = () => refreshing ??= (async () => {
1525
+ const { clientId, clientSecret } = getClientConfig();
1526
+ const tok = await postToken({ grant_type: "refresh_token", refresh_token: creds.refreshToken, client_id: clientId, client_secret: clientSecret });
1527
+ creds = { kind: "google", refreshToken: tok.refreshToken ?? creds.refreshToken, accessToken: tok.accessToken, expiresAt: tok.expiresAt };
1528
+ await ctx.saveCredentials(creds);
1529
+ return tok.accessToken;
1530
+ })().finally(() => refreshing = void 0);
1531
+ const token = async () => creds.accessToken && (creds.expiresAt ?? 0) > Date.now() + 6e4 ? creds.accessToken : refresh();
1532
+ async function api(method, path, query = {}, body) {
1533
+ const qs = new URLSearchParams(query).toString();
1534
+ const url = `${API}${path}${qs ? `?${qs}` : ""}`;
1535
+ const send = async (accessToken) => timedFetch(url, {
1536
+ method,
1537
+ headers: { Authorization: `Bearer ${accessToken}`, ...body ? { "Content-Type": "application/json" } : {} },
1538
+ body: body ? JSON.stringify(body) : void 0
1539
+ });
1540
+ let res = await send(await token());
1541
+ if (res.status === 401) res = await send(await refresh());
1542
+ if (res.status === 401) throw new AuthError("Google rejected the sign-in; sign in again");
1543
+ if (res.status === 204 || method === "DELETE" && res.status === 410) return void 0;
1544
+ if (!res.ok) {
1545
+ const err = await res.json().catch(() => ({}));
1546
+ throw new Error(`Google API ${res.status}: ${err.error?.message ?? res.statusText}`);
1547
+ }
1548
+ return await res.json();
1549
+ }
1550
+ const eventPath = (calendarId, eventId) => `/calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`;
1551
+ const rawOf = (e) => e.raw;
1552
+ const isOrganizer = (e) => {
1553
+ const org = rawOf(e)?.organizer;
1554
+ if (org?.self !== void 0) return org.self;
1555
+ return !e.organizer || e.organizer.email.toLowerCase() === ctx.email.toLowerCase();
1556
+ };
1557
+ const map = (g, calendarId) => mapEvent(g, ctx.accountId, calendarId);
1558
+ const fields = (event, prevAttendees, organizer) => {
1559
+ const body = { summary: event.title, location: event.location ?? null, description: event.description ?? null };
1560
+ if (organizer) {
1561
+ const prev = new Map((prevAttendees ?? []).map((a) => [a.email.toLowerCase(), a]));
1562
+ body.attendees = event.attendees.map((a) => prev.get(a.email.toLowerCase()) ?? { email: a.email, displayName: a.name });
1563
+ }
1564
+ return body;
1565
+ };
1566
+ async function remainingRecurrence(calendarId, series, recurrence, slot) {
1567
+ const rules = recurrence.filter((l) => l.startsWith("RRULE:"));
1568
+ if (!rules.some((l) => /;COUNT=|:COUNT=/i.test(l))) return rules;
1569
+ const cut = Date.parse(slot.dateTime ?? slot.date);
1570
+ let passed = 0;
1571
+ let pageToken;
1572
+ do {
1573
+ const page = await api("GET", `${eventPath(calendarId, series)}/instances`, {
1574
+ showDeleted: "true",
1575
+ timeMax: new Date(cut).toISOString(),
1576
+ maxResults: "2500",
1577
+ ...pageToken ? { pageToken } : {}
1578
+ });
1579
+ for (const g of page.items ?? []) {
1580
+ const t2 = g.originalStartTime ?? g.start;
1581
+ if (Date.parse(t2.dateTime ?? t2.date) < cut) passed++;
1582
+ }
1583
+ pageToken = page.nextPageToken;
1584
+ } while (pageToken);
1585
+ return rules.map((l) => l.replace(/COUNT=(\d+)/i, (_, n) => `COUNT=${Math.max(1, Number(n) - passed)}`));
1586
+ }
1587
+ return {
1588
+ async listCalendars() {
1589
+ const out = [];
1590
+ let pageToken;
1591
+ do {
1592
+ const page = await api(
1593
+ "GET",
1594
+ "/users/me/calendarList",
1595
+ pageToken ? { pageToken } : {}
1596
+ );
1597
+ for (const c of page.items ?? [])
1598
+ out.push({
1599
+ id: c.id,
1600
+ accountId: ctx.accountId,
1601
+ name: c.summaryOverride ?? c.summary ?? c.id,
1602
+ color: c.backgroundColor ?? "#4285f4",
1603
+ readOnly: c.accessRole !== "owner" && c.accessRole !== "writer"
1604
+ });
1605
+ pageToken = page.nextPageToken;
1606
+ } while (pageToken);
1607
+ return out;
1608
+ },
1609
+ async listEvents(calendarId, range) {
1610
+ const out = [];
1611
+ let pageToken;
1612
+ do {
1613
+ const page = await api("GET", eventPath(calendarId), {
1614
+ singleEvents: "true",
1615
+ timeMin: new Date(range.start).toISOString(),
1616
+ timeMax: new Date(range.end).toISOString(),
1617
+ maxResults: "2500",
1618
+ ...pageToken ? { pageToken } : {}
1619
+ });
1620
+ for (const g of page.items ?? []) if (g.status !== "cancelled") out.push(map(g, calendarId));
1621
+ pageToken = page.nextPageToken;
1622
+ } while (pageToken);
1623
+ return out;
1624
+ },
1625
+ async createEvent(calendarId, input) {
1626
+ const attendees = (input.attendees ?? []).map((email) => ({ email }));
1627
+ const rule = input.recurrence;
1628
+ const g = await api(
1629
+ "POST",
1630
+ eventPath(calendarId),
1631
+ { sendUpdates: attendees.length ? "all" : "none" },
1632
+ {
1633
+ summary: input.title,
1634
+ start: rule ? zoned(input.start, input.allDay, localZone()) : toGTime(input.start, input.allDay),
1635
+ end: rule ? zoned(input.end, input.allDay, localZone()) : toGTime(input.end, input.allDay),
1636
+ location: input.location,
1637
+ description: input.description,
1638
+ attendees,
1639
+ ...rule ? { recurrence: rruleLines(rule, input.allDay) } : {}
1640
+ }
1641
+ );
1642
+ return map(g, calendarId);
1643
+ },
1644
+ async updateEvent(event, scope = "one") {
1645
+ const organizer = isOrganizer(event);
1646
+ const q = { sendUpdates: organizer ? "all" : "none" };
1647
+ const series = event.recurringEventId;
1648
+ const before = rawOf(event)?.start;
1649
+ if (!series || scope === "one" || !before) {
1650
+ const rule2 = !series ? event.recurrence : void 0;
1651
+ const time = (iso2) => rule2 ? nulled(zoned(iso2, event.allDay, localZone())) : toGTime(iso2, event.allDay);
1652
+ const body = {
1653
+ ...fields(event, rawOf(event)?.attendees, organizer),
1654
+ start: time(event.start),
1655
+ end: time(event.end),
1656
+ ...rule2 ? { recurrence: rruleLines(rule2, event.allDay) } : {}
1657
+ };
1658
+ return map(await api("PATCH", eventPath(event.calendarId, event.id), q, body), event.calendarId);
1659
+ }
1660
+ const master = await api("GET", eventPath(event.calendarId, series));
1661
+ const timeZone = master.start.timeZone ?? localZone();
1662
+ const rule = event.recurrence;
1663
+ const slot = rawOf(event)?.originalStartTime ?? before;
1664
+ const first = master.start.dateTime ?? master.start.date;
1665
+ const at3 = slot.dateTime ?? slot.date;
1666
+ if (scope === "all" || Date.parse(at3) <= Date.parse(first)) {
1667
+ const start2 = rule === null ? zoned(event.start, event.allDay, timeZone) : shiftAnchor(master.start, before, event.start, event.allDay, timeZone);
1668
+ const body = {
1669
+ ...fields(event, master.attendees, organizer),
1670
+ start: nulled(start2),
1671
+ end: nulled(endFrom(start2, event, timeZone)),
1672
+ // A new RRULE replaces the old ones; EXDATE/RDATE lines stay.
1673
+ ...rule === void 0 ? {} : { recurrence: rule ? [...rruleLines(rule, event.allDay), ...(master.recurrence ?? []).filter((l) => !l.startsWith("RRULE:"))] : [] }
1674
+ };
1675
+ await api("PATCH", eventPath(event.calendarId, series), q, body);
1676
+ return event;
1677
+ }
1678
+ const recurrence = rule === void 0 ? await remainingRecurrence(event.calendarId, series, master.recurrence ?? [], slot) : rule ? rruleLines(rule, event.allDay) : void 0;
1679
+ await api("PATCH", eventPath(event.calendarId, series), q, { recurrence: truncateRecurrence(master.recurrence ?? [], slot) });
1680
+ const start = shiftAnchor(slot, before, event.start, event.allDay, timeZone);
1681
+ const created = await api("POST", eventPath(event.calendarId), q, {
1682
+ ...fields(event, master.attendees, organizer),
1683
+ start,
1684
+ end: endFrom(start, event, timeZone),
1685
+ ...recurrence ? { recurrence } : {}
1686
+ });
1687
+ return { ...map(created, event.calendarId), start: event.start, end: event.end, recurringEventId: recurrence ? created.id : void 0 };
1688
+ },
1689
+ async deleteEvent(event, scope = "one") {
1690
+ const q = { sendUpdates: isOrganizer(event) ? "all" : "none" };
1691
+ const series = event.recurringEventId;
1692
+ if (!series || scope === "one") return void await api("DELETE", eventPath(event.calendarId, event.id), q);
1693
+ const cutoff = rawOf(event)?.originalStartTime;
1694
+ if (scope === "following" && cutoff) {
1695
+ const master = await api("GET", eventPath(event.calendarId, series));
1696
+ const first = master.start.dateTime ?? master.start.date;
1697
+ const at3 = cutoff.dateTime ?? cutoff.date;
1698
+ if (Date.parse(at3) > Date.parse(first)) {
1699
+ const recurrence = truncateRecurrence(master.recurrence ?? [], cutoff);
1700
+ return void await api("PATCH", eventPath(event.calendarId, series), q, { recurrence });
1701
+ }
1702
+ }
1703
+ await api("DELETE", eventPath(event.calendarId, series), q);
1704
+ },
1705
+ async getRecurrence(event) {
1706
+ if (!event.recurringEventId) return null;
1707
+ const master = await api("GET", eventPath(event.calendarId, event.recurringEventId));
1708
+ const line = master.recurrence?.find((l) => l.startsWith("RRULE:"));
1709
+ return line ? fromRRule(line, master.start.dateTime ?? master.start.date) : null;
1710
+ },
1711
+ async respond(event, status) {
1712
+ const attendees = rawOf(event)?.attendees ?? event.attendees.map((a) => ({ email: a.email, displayName: a.name, responseStatus: a.status, self: a.self, organizer: a.organizer }));
1713
+ if (!attendees.some((a) => a.self)) throw new Error("This account is not an attendee of the event");
1714
+ const g = await api(
1715
+ "PATCH",
1716
+ eventPath(event.calendarId, event.id),
1717
+ { sendUpdates: "all" },
1718
+ { attendees: attendees.map((a) => a.self ? { ...a, responseStatus: status } : a) }
1719
+ );
1720
+ return map(g, event.calendarId);
1721
+ }
1722
+ };
1723
+ }
1724
+
1725
+ // ../../packages/core/src/api.ts
1726
+ var PALETTE = ["#bd93f9", "#50fa7b", "#8be9fd", "#ff79c6", "#ffb86c", "#f1fa8c"];
1727
+ var id = z.string().min(1).max(1024);
1728
+ var isoDateOrTime = z.union([z.iso.datetime({ offset: true }), z.iso.date()]);
1729
+ var color = z.string().regex(/^#[0-9a-fA-F]{6}$/);
1730
+ var text = (max2) => z.string().max(max2);
1731
+ var ordered = (v) => Date.parse(v.end) >= Date.parse(v.start);
1732
+ var Range = z.object({ start: isoDateOrTime, end: isoDateOrTime }).refine(ordered, "end before start");
1733
+ var Recurrence = z.object({
1734
+ freq: z.enum(["daily", "weekly", "monthly", "yearly"]),
1735
+ interval: z.number().int().min(1).max(999).optional(),
1736
+ byDay: z.array(z.enum(["MO", "TU", "WE", "TH", "FR", "SA", "SU"])).max(7).optional(),
1737
+ until: z.iso.date().optional(),
1738
+ count: z.number().int().min(1).max(999).optional(),
1739
+ // An RRULE value kept from the provider: one line of plain RRULE parts only.
1740
+ rule: z.string().max(1e3).regex(/^[A-Z0-9=;,+-]+$/i).optional()
1741
+ }).strict();
1742
+ var NewEvent = z.object({
1743
+ accountId: id,
1744
+ calendarId: id,
1745
+ title: text(1e3),
1746
+ start: isoDateOrTime,
1747
+ end: isoDateOrTime,
1748
+ allDay: z.boolean(),
1749
+ location: text(1e3).optional(),
1750
+ description: text(2e4).optional(),
1751
+ attendees: z.array(z.email()).max(500).optional(),
1752
+ recurrence: Recurrence.optional()
1753
+ }).refine(ordered, "end before start");
1754
+ var Attendee = z.object({
1755
+ email: text(320).min(1),
1756
+ name: text(500).optional(),
1757
+ status: z.enum(["accepted", "declined", "tentative", "needsAction"]),
1758
+ self: z.boolean().optional(),
1759
+ organizer: z.boolean().optional()
1760
+ });
1761
+ var EventRef = z.object({ id, accountId: id, calendarId: id });
1762
+ var EventEdit = EventRef.extend({
1763
+ title: text(1e3),
1764
+ start: isoDateOrTime,
1765
+ end: isoDateOrTime,
1766
+ allDay: z.boolean(),
1767
+ location: text(1e3).optional(),
1768
+ description: text(2e4).optional(),
1769
+ attendees: z.array(Attendee).max(500),
1770
+ // Absent keeps the series' rule; null stops repeating.
1771
+ recurrence: Recurrence.nullish()
1772
+ }).refine(ordered, "end before start");
1773
+ var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
1774
+ var CaldavInput = z.object({
1775
+ label: text(200).min(1),
1776
+ serverUrl: z.string().refine((u) => {
1777
+ if (!URL.canParse(u)) return false;
1778
+ const { protocol, hostname } = new URL(u);
1779
+ return protocol === "https:" || protocol === "http:" && LOOPBACK.has(hostname);
1780
+ }, "server URL must be https"),
1781
+ username: text(500).min(1),
1782
+ password: text(2e3).min(1),
1783
+ color: color.optional()
1784
+ });
1785
+ var ReauthInput = z.object({ password: text(2e3).min(1) }).strict();
1786
+ var AccountPatch = z.object({ label: text(200).min(1).optional(), color: color.optional() }).strict();
1787
+ var RsvpStatus = z.enum(["accepted", "declined", "tentative"]);
1788
+ var Scope = z.enum(["one", "following", "all"]).nullish().transform((s) => s ?? "one");
1789
+ function createApi(store, sync, deps) {
1790
+ const account = (accountId) => {
1791
+ const a = store.get(id.parse(accountId));
1792
+ if (!a) throw new Error("unknown account");
1793
+ return a;
1794
+ };
1795
+ const syncOne = (accountId) => {
1796
+ sync.syncNow(accountId, { quiet: true, fresh: true }).catch((e) => console.error(`sync ${accountId} failed`, e));
1797
+ };
1798
+ const applyLocal = (accountId, fn) => {
1799
+ try {
1800
+ store.patchCache(accountId, (c) => ({ ...c, events: fn(c.events) }));
1801
+ sync.edited(accountId);
1802
+ deps.onChanged?.(accountId);
1803
+ } catch (e) {
1804
+ console.error(`local update of ${accountId} failed`, e);
1805
+ }
1806
+ };
1807
+ const upsert = (ev, accountId, calendarId) => {
580
1808
  const own = { ...ev, accountId, calendarId };
581
1809
  applyLocal(accountId, (events) => [...events.filter((e) => e.id !== own.id), own]);
582
1810
  };
@@ -661,7 +1889,10 @@ function createApi(store, sync, deps) {
661
1889
  },
662
1890
  events: {
663
1891
  // `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),
1892
+ // Caches synced before `conferenceUrl` existed still hold the Meet link in the Google resource.
1893
+ list: async (raw) => queryEvents(store, Range.parse(raw), false).map(
1894
+ ({ raw: r, ...e }) => e.conferenceUrl || !r || typeof r !== "object" ? e : { ...e, conferenceUrl: conferenceUrlOf(r) }
1895
+ ),
665
1896
  create: async (raw) => {
666
1897
  const input = NewEvent.parse(raw);
667
1898
  const a = account(input.accountId);
@@ -892,158 +2123,25 @@ var AccountStore = class {
892
2123
  return [];
893
2124
  }
894
2125
  }
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;
2126
+ setCalendarVisible(id2, calendarId, visible) {
2127
+ return this.serial(async () => {
2128
+ this.require(id2);
2129
+ const hidden = new Set(this.hiddenCalendars(id2));
2130
+ if (visible) hidden.delete(calendarId);
2131
+ else hidden.add(calendarId);
2132
+ const dir = this.accountDir(id2);
2133
+ await mkdir(dir, { recursive: true });
2134
+ await writeAtomic(join4(dir, "prefs.json"), JSON.stringify({ hidden: [...hidden] }));
2135
+ });
988
2136
  }
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
- }
2137
+ };
2138
+
2139
+ // ../../packages/core/src/mock/mockApi.ts
2140
+ import { addDays as addDays2, format as format5 } from "date-fns";
1045
2141
 
1046
2142
  // ../../packages/core/src/mock/MockProvider.ts
2143
+ import { randomUUID as randomUUID3 } from "crypto";
2144
+ import { addDays, differenceInCalendarDays, differenceInCalendarMonths, format as format4, parseISO as parseISO4, startOfWeek } from "date-fns";
1047
2145
  var MAX_INSTANCES = 60;
1048
2146
  function expand(first, r, series) {
1049
2147
  const start = parseISO4(first.start);
@@ -1055,7 +2153,7 @@ function expand(first, r, series) {
1055
2153
  const n = differenceInCalendarDays(d, start);
1056
2154
  if (r.freq === "daily") return n % every === 0;
1057
2155
  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;
2156
+ return days.includes(weekdayOf(format4(d, "yyyy-MM-dd"))) && differenceInCalendarDays(startOfWeek(d, { weekStartsOn: 1 }), startOfWeek(start, { weekStartsOn: 1 })) / 7 % every === 0;
1059
2157
  const months = differenceInCalendarMonths(d, start);
1060
2158
  if (d.getDate() !== start.getDate()) return false;
1061
2159
  return r.freq === "monthly" ? months % every === 0 : months % (12 * every) === 0;
@@ -1065,8 +2163,8 @@ function expand(first, r, series) {
1065
2163
  const d = addDays(start, n);
1066
2164
  if (until && differenceInCalendarDays(d, until) > 0) break;
1067
2165
  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();
2166
+ const s = first.allDay ? format4(d, "yyyy-MM-dd") : d.toISOString();
2167
+ const e = first.allDay ? format4(addDays(parseISO4(first.end), n), "yyyy-MM-dd") : new Date(d.getTime() + length).toISOString();
1070
2168
  out.push({ ...first, id: `${series}@${s}`, start: s, end: e, recurringEventId: series, recurrence: void 0 });
1071
2169
  }
1072
2170
  return out;
@@ -1087,9 +2185,9 @@ var MockProvider = class {
1087
2185
  return structuredClone(this.calendars);
1088
2186
  }
1089
2187
  async listEvents(calendarId, range) {
1090
- const t = (iso2) => parseISO4(iso2).getTime();
2188
+ const t2 = (iso2) => parseISO4(iso2).getTime();
1091
2189
  return structuredClone(
1092
- this.events.filter((e) => e.calendarId === calendarId && t(e.end) > t(range.start) && t(e.start) < t(range.end))
2190
+ this.events.filter((e) => e.calendarId === calendarId && t2(e.end) > t2(range.start) && t2(e.start) < t2(range.end))
1093
2191
  );
1094
2192
  }
1095
2193
  async createEvent(calendarId, input) {
@@ -1229,8 +2327,11 @@ function createMockApi(onChanged) {
1229
2327
  seed(personal, "p-main", { title: "Morning run", start: iso(d, 7), end: iso(d, 7, 45), recurringEventId: "run-series" });
1230
2328
  }
1231
2329
  personal.rules.set("run-series", { freq: "daily", count: 7 });
2330
+ seed(personal, "p-main", { title: "Ukraine - Portugal", start: iso(5, 12, 30), end: iso(5, 13), conferenceUrl: "https://meet.google.com/zon-fdwf-hnk" });
2331
+ seed(personal, "p-main", { title: "Catch-up", start: iso(5, 16), end: iso(5, 16, 30), location: "https://meet.namechip.net/catch-up" });
2332
+ 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
2333
  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");
2334
+ const day = (offset) => format5(addDays2(/* @__PURE__ */ new Date(), offset), "yyyy-MM-dd");
1234
2335
  seed(personal, "p-holidays", { title: "Holiday", start: day(0), end: day(1), allDay: true });
1235
2336
  for (const title2 of ["Conference", "Team offsite", "Release freeze", "Birthday"])
1236
2337
  seed(personal, "p-main", { title: title2, start: day(3), end: day(4), allDay: true });
@@ -1356,16 +2457,16 @@ function parse(ics) {
1356
2457
  }
1357
2458
  return root;
1358
2459
  }
1359
- var toIso = (t) => t.isDate ? t.toString() : t.toJSDate().toISOString();
2460
+ var toIso = (t2) => t2.isDate ? t2.toString() : t2.toJSDate().toISOString();
1360
2461
  function fromIso(iso2, allDay) {
1361
2462
  if (allDay) return ICAL.Time.fromDateString(iso2.slice(0, 10));
1362
2463
  const d = new Date(iso2);
1363
2464
  if (Number.isNaN(d.getTime())) throw new Error(`Invalid date: ${iso2}`);
1364
2465
  return ICAL.Time.fromJSDate(d, true);
1365
2466
  }
1366
- function setTime(vevent, name, t) {
2467
+ function setTime(vevent, name, t2) {
1367
2468
  vevent.removeAllProperties(name);
1368
- vevent.addPropertyWithValue(name, t);
2469
+ vevent.addPropertyWithValue(name, t2);
1369
2470
  }
1370
2471
  function setText(vevent, name, value) {
1371
2472
  vevent.removeAllProperties(name);
@@ -1477,15 +2578,15 @@ function applyRule(root, master, event) {
1477
2578
  return;
1478
2579
  }
1479
2580
  setRule(master, event.recurrence, event.allDay);
1480
- const ms = (t) => t.toJSDate().getTime();
2581
+ const ms = (t2) => t2.toJSDate().getTime();
1481
2582
  const rid = (o) => o.getFirstPropertyValue("recurrence-id");
1482
- const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t) => t instanceof ICAL.Time);
2583
+ const exdates = master.getAllProperties("exdate").flatMap((p) => p.getValues()).filter((t2) => t2 instanceof ICAL.Time);
1483
2584
  const last = Math.max(-Infinity, ...overrides.map((o) => ms(rid(o))), ...exdates.map(ms));
1484
2585
  const slots2 = /* @__PURE__ */ new Set();
1485
2586
  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));
2587
+ for (let t2 = it.next(), n = 0; t2 && ms(t2) <= last && n < MAX_INSTANCES2; t2 = it.next(), n++) slots2.add(ms(t2));
1487
2588
  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);
2589
+ mapDates(master, "exdate", (t2) => slots2.has(ms(t2)) ? t2 : null);
1489
2590
  }
1490
2591
  function setAttendees(v, emails, email) {
1491
2592
  const wanted = [...new Set(emails.map(cleanEmail).filter(Boolean))];
@@ -1520,8 +2621,8 @@ function findOverride(root, rid) {
1520
2621
  const master = findMaster(root);
1521
2622
  const want = master ? ridTime(master, rid).time.toJSDate().getTime() : NaN;
1522
2623
  return root.getAllSubcomponents("vevent").find((v) => {
1523
- const t = v.getFirstPropertyValue("recurrence-id");
1524
- return !!t && (String(t) === rid || t.toJSDate().getTime() === want);
2624
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2625
+ return !!t2 && (String(t2) === rid || t2.toJSDate().getTime() === want);
1525
2626
  });
1526
2627
  }
1527
2628
  function ridProp(master, name, rid) {
@@ -1568,27 +2669,27 @@ function bump(v) {
1568
2669
  v.updatePropertyWithValue("sequence", Number(v.getFirstPropertyValue("sequence") ?? 0) + 1);
1569
2670
  touch(v);
1570
2671
  }
1571
- function timeProp(name, t) {
2672
+ function timeProp(name, t2) {
1572
2673
  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);
2674
+ p.setValue(t2);
2675
+ const tzid = t2.zone?.tzid;
2676
+ if (!t2.isDate && tzid && tzid !== "UTC" && tzid !== "floating") p.setParameter("tzid", tzid);
1576
2677
  return p;
1577
2678
  }
1578
- function replaceTime(v, name, t) {
2679
+ function replaceTime(v, name, t2) {
1579
2680
  v.removeAllProperties(name);
1580
- return timeProp(name, t);
2681
+ return timeProp(name, t2);
1581
2682
  }
1582
2683
  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();
2684
+ var dateOf = (t2) => ICAL.Time.fromData({ year: t2.year, month: t2.month, day: t2.day, isDate: true });
2685
+ var daysBetween2 = (a, b) => Math.round(dateOf(b).subtractDate(dateOf(a)).toSeconds() / 86400);
2686
+ function addDays3(t2, days) {
2687
+ const x = t2.clone();
1587
2688
  x.adjust(days, 0, 0, 0);
1588
2689
  return x;
1589
2690
  }
1590
2691
  function spanFrom(start, event) {
1591
- if (event.allDay) return [start, addDays3(start, daysBetween(fromIso(event.start, true), fromIso(event.end, true)))];
2692
+ if (event.allDay) return [start, addDays3(start, daysBetween2(fromIso(event.start, true), fromIso(event.end, true)))];
1592
2693
  const end = start.clone();
1593
2694
  end.adjust(0, 0, 0, (Date.parse(event.end) - Date.parse(event.start)) / 1e3);
1594
2695
  return [start, end];
@@ -1596,23 +2697,23 @@ function spanFrom(start, event) {
1596
2697
  function slotShift(master, before, event) {
1597
2698
  const anchor = dtstart(master);
1598
2699
  const zone = anchor.isDate ? void 0 : anchor.zone;
1599
- const local = (t) => zone && !t.isDate ? t.convertToZone(zone) : t.clone();
2700
+ const local = (t2) => zone && !t2.isDate ? t2.convertToZone(zone) : t2.clone();
1600
2701
  const after = fromIso(event.start, event.allDay);
1601
2702
  if (event.allDay) {
1602
- const days2 = daysBetween(local(before), after);
1603
- return (t) => addDays3(dateOf(local(t)), days2);
2703
+ const days2 = daysBetween2(local(before), after);
2704
+ return (t2) => addDays3(dateOf(local(t2)), days2);
1604
2705
  }
1605
2706
  if (!anchor.isDate) {
1606
2707
  const seconds = (zone ? after.convertToZone(zone) : after).subtractDate(local(before)).toSeconds();
1607
- return (t) => {
1608
- const x = local(t);
2708
+ return (t2) => {
2709
+ const x = local(t2);
1609
2710
  x.adjust(0, 0, 0, seconds);
1610
2711
  return x;
1611
2712
  };
1612
2713
  }
1613
- const days = daysBetween(before, after);
1614
- return (t) => {
1615
- const d = addDays3(dateOf(t), days);
2714
+ const days = daysBetween2(before, after);
2715
+ return (t2) => {
2716
+ const d = addDays3(dateOf(t2), days);
1616
2717
  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
2718
  };
1618
2719
  }
@@ -1628,8 +2729,8 @@ function mapDates(v, name, map) {
1628
2729
  out.push(keep);
1629
2730
  continue;
1630
2731
  }
1631
- const t = map(value);
1632
- if (t) out.push(timeProp(name, t));
2732
+ const t2 = map(value);
2733
+ if (t2) out.push(timeProp(name, t2));
1633
2734
  }
1634
2735
  }
1635
2736
  v.removeAllProperties(name);
@@ -1687,10 +2788,10 @@ function applySplitFollowing(event, email, uid) {
1687
2788
  const edited = findOverride(root, raw.recurrenceId);
1688
2789
  const shift = slotShift(master, instanceStart(master, edited, raw.recurrenceId), event);
1689
2790
  for (const v of root.getAllSubcomponents("vevent")) {
1690
- const t = v.getFirstPropertyValue("recurrence-id");
1691
- if (t && t.toJSDate().getTime() < cut) root.removeSubcomponent(v);
2791
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2792
+ if (t2 && t2.toJSDate().getTime() < cut) root.removeSubcomponent(v);
1692
2793
  }
1693
- const later = (t) => t.toJSDate().getTime() >= cut ? t : null;
2794
+ const later = (t2) => t2.toJSDate().getTime() >= cut ? t2 : null;
1694
2795
  mapDates(master, "exdate", later);
1695
2796
  mapDates(master, "rdate", later);
1696
2797
  const first = dtstart(master);
@@ -1699,7 +2800,7 @@ function applySplitFollowing(event, email, uid) {
1699
2800
  if (!r.count) continue;
1700
2801
  const it = r.iterator(first);
1701
2802
  let passed = 0;
1702
- for (let t = it.next(); t && t.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t = it.next()) passed++;
2803
+ for (let t2 = it.next(); t2 && t2.toJSDate().getTime() < cut && passed < MAX_INSTANCES2; t2 = it.next()) passed++;
1703
2804
  r.count = Math.max(1, r.count - passed);
1704
2805
  p.setValue(r);
1705
2806
  }
@@ -1733,601 +2834,198 @@ function applyDeleteFollowing(raw) {
1733
2834
  const cut = time.toJSDate().getTime();
1734
2835
  if (cut <= master.getFirstPropertyValue("dtstart").toJSDate().getTime()) return null;
1735
2836
  for (const v of root.getAllSubcomponents("vevent")) {
1736
- const t = v.getFirstPropertyValue("recurrence-id");
1737
- if (t && t.toJSDate().getTime() >= cut) root.removeSubcomponent(v);
2837
+ const t2 = v.getFirstPropertyValue("recurrence-id");
2838
+ if (t2 && t2.toJSDate().getTime() >= cut) root.removeSubcomponent(v);
1738
2839
  }
1739
2840
  let until = time.clone();
1740
2841
  if (until.isDate) until.adjust(-1, 0, 0, 0);
1741
2842
  else {
1742
2843
  until.adjust(0, 0, 0, -1);
1743
2844
  if (until.zone !== ICAL.Timezone.localTimezone) until = until.convertToZone(ICAL.Timezone.utcTimezone);
1744
- }
1745
- for (const p of master.getAllProperties("rrule")) {
1746
- const r = p.getFirstValue().clone();
1747
- 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") };
2845
+ }
2846
+ for (const p of master.getAllProperties("rrule")) {
2847
+ const r = p.getFirstValue().clone();
2848
+ r.count = null;
2849
+ r.until = until;
2850
+ p.setValue(r);
2851
+ }
2852
+ master.updatePropertyWithValue("sequence", Number(master.getFirstPropertyValue("sequence") ?? 0) + 1);
2853
+ touch(master);
2854
+ return root.toString();
1949
2855
  }
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();
2856
+ function applyRespond(raw, email, status) {
2857
+ const root = parse(raw.ics);
2858
+ let found = false;
2859
+ for (const v of root.getAllSubcomponents("vevent")) {
2860
+ for (const p of v.getAllProperties("attendee")) {
2861
+ if (!sameEmail(p.getFirstValue(), email)) continue;
2862
+ p.setParameter("partstat", PARTSTAT_OUT[status]);
2863
+ p.removeParameter("rsvp");
2864
+ p.removeParameter("schedule-status");
2865
+ found = true;
2866
+ }
2867
+ if (found) v.updatePropertyWithValue("dtstamp", ICAL.Time.now());
2868
+ }
2869
+ if (!found) throw new Error("This account is not an attendee of the event");
2870
+ return root.toString();
1964
2871
  }
1965
- function emailFromIdToken(idToken) {
2872
+
2873
+ // ../../packages/core/src/providers/caldav/index.ts
2874
+ var FALLBACK_COLOR = "#8e8e93";
2875
+ var DAY2 = 864e5;
2876
+ async function connect2(serverUrl, username, password) {
1966
2877
  try {
1967
- const payload = JSON.parse(Buffer.from(idToken.split(".")[1], "base64url").toString("utf8"));
1968
- return typeof payload.email === "string" ? payload.email : void 0;
2878
+ new URL(serverUrl);
1969
2879
  } catch {
1970
- return void 0;
1971
- }
1972
- }
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);
2880
+ throw new Error(`Invalid server URL: ${serverUrl}`);
1978
2881
  }
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
2882
+ const headers = getBasicAuthHeaders({ username, password });
2883
+ let unauthorized = false;
2884
+ const trackingFetch = async (input, init) => {
2885
+ const res = await timedFetch(input, init);
2886
+ if (res.status === 401) unauthorized = true;
2887
+ return res;
1986
2888
  };
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
2889
  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
2890
+ const account = await createAccount({
2891
+ account: { serverUrl, accountType: "caldav", credentials: { username, password } },
2892
+ headers,
2893
+ fetch: trackingFetch
2048
2894
  });
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();
2895
+ return { account, headers };
2896
+ } catch (e) {
2897
+ if (unauthorized) throw new AuthError("CalDAV login failed: wrong username or password");
2898
+ throw new Error(`Could not find a CalDAV service at ${serverUrl}: ${e.message}`);
2055
2899
  }
2056
2900
  }
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
- };
2901
+ function toCalendar(c, accountId) {
2902
+ const name = typeof c.displayName === "string" && c.displayName ? c.displayName : decodeURIComponent(c.url.split("/").filter(Boolean).pop() ?? c.url);
2903
+ const color2 = typeof c.calendarColor === "string" && /^#[0-9a-f]{6}/i.test(c.calendarColor) ? c.calendarColor.slice(0, 7) : FALLBACK_COLOR;
2904
+ const privileges = JSON.stringify(c.projectedProps?.currentUserPrivilegeSet ?? "");
2905
+ const readOnly = privileges.includes("privilege") && !/"(write|writeContent|all)"/.test(privileges);
2906
+ return { id: c.url, accountId, name, color: color2, readOnly };
2096
2907
  }
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(";")}`;
2908
+ var CALENDAR_PROPS = {
2909
+ "d:displayname": {},
2910
+ "ca:calendar-color": {},
2911
+ "cs:getctag": {},
2912
+ "d:resourcetype": {},
2913
+ "c:supported-calendar-component-set": {},
2914
+ "d:sync-token": {},
2915
+ "d:current-user-privilege-set": {}
2916
+ };
2917
+ async function listVeventCalendars(conn) {
2918
+ const cals = await fetchCalendars({
2919
+ account: conn.account,
2920
+ headers: conn.headers,
2921
+ props: CALENDAR_PROPS,
2922
+ projectedProps: { currentUserPrivilegeSet: true },
2923
+ fetch: davFetch
2103
2924
  });
2925
+ return cals.filter((c) => !c.components?.length || c.components.includes("VEVENT"));
2104
2926
  }
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
2927
+ var davFetch = async (input, init) => {
2928
+ const res = await timedFetch(input, init);
2929
+ if (res.status === 401) throw new AuthError("CalDAV login failed: the password was changed or revoked");
2930
+ return res;
2931
+ };
2932
+ function check(res, what) {
2933
+ if (res.ok) return;
2934
+ if (res.status === 412) throw new Error(`${what} failed: the event was changed on the server, refresh and try again`);
2935
+ throw new Error(`${what} failed: ${res.status} ${res.statusText}`);
2936
+ }
2937
+ var createCaldavProvider = (ctx) => {
2938
+ if (ctx.credentials.kind !== "caldav") throw new Error("CalDAV provider needs caldav credentials");
2939
+ const { serverUrl, username, password } = ctx.credentials;
2940
+ let conn;
2941
+ const getConn = () => {
2942
+ conn ??= connect2(serverUrl, username, password).catch((e) => {
2943
+ conn = void 0;
2944
+ throw e;
2147
2945
  });
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;
2946
+ return conn;
2173
2947
  };
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)}`));
2948
+ const mapCtx = (calendarId) => ({ accountId: ctx.accountId, calendarId, email: ctx.email });
2949
+ async function reload(calendarId, href, id2, around) {
2950
+ const { headers } = await getConn();
2951
+ const [obj] = await fetchCalendarObjects({ calendar: { url: calendarId }, objectUrls: [href], headers, urlFilter: () => true, fetch: davFetch });
2952
+ if (!obj?.data) throw new Error("Saved event could not be read back from the server");
2953
+ const range = { start: new Date(new Date(around.start).getTime() - DAY2).toISOString(), end: new Date(new Date(around.end).getTime() + DAY2).toISOString() };
2954
+ const events = parseEvents(obj.data, id2 === void 0 ? obj.url : href, obj.etag, mapCtx(calendarId), range);
2955
+ const at3 = Date.parse(around.start);
2956
+ const ev = id2 === void 0 ? events.find((e) => Date.parse(e.start) === at3) ?? events[0] : events.find((e) => e.id === id2);
2957
+ if (!ev) throw new Error("Saved event not found on the server");
2958
+ return ev;
2194
2959
  }
2195
- return {
2960
+ const put = async (event, ics, what) => {
2961
+ const { headers } = await getConn();
2962
+ const raw = event.raw;
2963
+ check(await updateCalendarObject({ calendarObject: { url: raw.href, data: ics, etag: event.etag }, headers, fetch: davFetch }), what);
2964
+ };
2965
+ const provider = {
2196
2966
  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;
2967
+ const c = await getConn();
2968
+ return (await listVeventCalendars(c)).map((cal) => toCalendar(cal, ctx.accountId));
2216
2969
  },
2217
2970
  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;
2971
+ const { headers } = await getConn();
2972
+ const objs = await fetchCalendarObjects({ calendar: { url: calendarId }, timeRange: range, headers, urlFilter: () => true, fetch: davFetch });
2973
+ return objs.flatMap((o) => o.data ? parseEvents(o.data, o.url, o.etag, mapCtx(calendarId), range) : []);
2232
2974
  },
2233
2975
  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);
2976
+ const { headers } = await getConn();
2977
+ const uid = randomUUID4();
2978
+ const filename = `${uid}.ics`;
2979
+ const ics = buildIcs(uid, input, ctx.email);
2980
+ check(await createCalendarObject({ calendar: { url: calendarId }, filename, iCalString: ics, headers, fetch: davFetch }), "Create event");
2981
+ const href = new URL(filename, calendarId.endsWith("/") ? calendarId : `${calendarId}/`).href;
2982
+ return reload(calendarId, href, void 0, input);
2251
2983
  },
2252
2984
  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);
2985
+ const raw = event.raw;
2986
+ if (!raw.recurrenceId || scope === "one") {
2987
+ await put(event, applyUpdate(event, ctx.email), "Update event");
2988
+ return reload(event.calendarId, raw.href, event.recurrence ? void 0 : event.id, event);
2267
2989
  }
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;
2990
+ const uid = randomUUID4();
2991
+ const split = scope === "following" ? applySplitFollowing(event, ctx.email, uid) : null;
2992
+ if (!split) {
2993
+ await put(event, applyUpdateSeries(event, ctx.email), "Update event");
2994
+ return reload(event.calendarId, raw.href, void 0, event);
2285
2995
  }
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 };
2996
+ await put(event, split.rest, "Update event");
2997
+ const { headers } = await getConn();
2998
+ const filename = `${uid}.ics`;
2999
+ check(await createCalendarObject({ calendar: { url: event.calendarId }, filename, iCalString: split.next, headers, fetch: davFetch }), "Update event");
3000
+ const href = new URL(filename, event.calendarId.endsWith("/") ? event.calendarId : `${event.calendarId}/`).href;
3001
+ return reload(event.calendarId, href, void 0, event);
2296
3002
  },
2297
3003
  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);
3004
+ const raw = event.raw;
3005
+ const rest = !raw.recurrenceId || scope === "all" ? null : scope === "following" ? applyDeleteFollowing(raw) : applyDeleteInstance(raw);
3006
+ if (rest) return put(event, rest, "Delete event");
3007
+ const { headers } = await getConn();
3008
+ check(await deleteCalendarObject({ calendarObject: { url: raw.href, etag: event.etag }, headers, fetch: davFetch }), "Delete event");
2312
3009
  },
2313
3010
  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;
3011
+ return recurrenceOf(event.raw);
2318
3012
  },
2319
3013
  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);
3014
+ await put(event, applyRespond(event.raw, ctx.email, status), "Respond");
3015
+ return reload(event.calendarId, event.raw.href, event.id, event);
2329
3016
  }
2330
3017
  };
3018
+ return provider;
3019
+ };
3020
+ async function verifyCaldav(input) {
3021
+ const c = await connect2(input.serverUrl.trim(), input.username, input.password);
3022
+ await listVeventCalendars(c);
3023
+ const addresses = await fetchCalendarUserAddresses({ account: c.account, headers: c.headers, fetch: davFetch }).catch(() => []);
3024
+ const mails = addresses.filter((a) => /^mailto:/i.test(a)).map(cleanEmail);
3025
+ const login = cleanEmail(input.username);
3026
+ const email = mails.find((m) => m === login) ?? mails[0] ?? (login.includes("@") ? login : void 0);
3027
+ if (!email) throw new Error("Could not determine the account email; use your full email address as the username");
3028
+ return { email };
2331
3029
  }
2332
3030
 
2333
3031
  // ../../packages/core/src/providers/google/index.ts
@@ -2661,7 +3359,7 @@ import {
2661
3359
  addDays as addDays4,
2662
3360
  addMonths as addMonths2,
2663
3361
  addWeeks,
2664
- format as format5,
3362
+ format as format6,
2665
3363
  isSameDay as isSameDay2,
2666
3364
  parseISO as parseISO5,
2667
3365
  startOfDay,
@@ -2746,11 +3444,11 @@ function shiftDate(view, date, dir) {
2746
3444
  if (view === "week") return addWeeks(date, dir);
2747
3445
  return addMonths2(date, dir);
2748
3446
  }
2749
- function rangeLabel(a, b) {
3447
+ function rangeLabel(a, b, locale = "en") {
2750
3448
  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")}`;
3449
+ return `${fmt(locale, a, head)} \u2013 ${fmt(locale, b, "d MMM yyyy")}`;
2752
3450
  }
2753
- var ymd = (d) => format5(d, "yyyy-MM-dd");
3451
+ var ymd = (d) => format6(d, "yyyy-MM-dd");
2754
3452
 
2755
3453
  // ../../packages/core/src/logic/visible.ts
2756
3454
  var key2 = (accountId, calendarId) => `${accountId}/${calendarId}`;
@@ -2829,8 +3527,8 @@ function useNav(initial = { view: "agenda", date: /* @__PURE__ */ new Date() })
2829
3527
  function useNow(ms = 3e4) {
2830
3528
  const [now, setNow] = useState(() => /* @__PURE__ */ new Date());
2831
3529
  useEffect(() => {
2832
- const t = setInterval(() => setNow(/* @__PURE__ */ new Date()), ms);
2833
- return () => clearInterval(t);
3530
+ const t2 = setInterval(() => setNow(/* @__PURE__ */ new Date()), ms);
3531
+ return () => clearInterval(t2);
2834
3532
  }, [ms]);
2835
3533
  return now;
2836
3534
  }
@@ -2852,9 +3550,9 @@ import { Box as Box11, Text as Text11, useApp, useStdin as useStdin2 } from "ink
2852
3550
  import { addDays as addDays11, differenceInMinutes as differenceInMinutes3, format as format13, isSameMonth as isSameMonth2, max, set, startOfDay as startOfDay7 } from "date-fns";
2853
3551
 
2854
3552
  // ../../packages/core/src/logic/editor.ts
2855
- import { addDays as addDays6, addHours, format as format6, parseISO as parseISO6, startOfHour } from "date-fns";
3553
+ import { addDays as addDays6, addHours, format as format7, parseISO as parseISO6, startOfHour } from "date-fns";
2856
3554
  var LOCAL = "yyyy-MM-dd'T'HH:mm";
2857
- var toLocalInput = (iso2) => format6(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
3555
+ var toLocalInput = (iso2) => format7(iso2.length === 10 ? parseISO6(iso2) : new Date(iso2), LOCAL);
2858
3556
  var fromLocalInput = (local) => new Date(local).toISOString();
2859
3557
  var writableCalendars = (calendars, accountId) => calendars.filter((c) => c.accountId === accountId && !c.readOnly);
2860
3558
  var writableAccounts = (accounts, calendars) => accounts.filter((a) => writableCalendars(calendars, a.id).length > 0);
@@ -2864,8 +3562,8 @@ var splitEmails = (text2) => text2.split(/[\s,;]+/).map((s) => s.trim()).filter(
2864
3562
  var isEmail = (s) => EMAIL.test(s);
2865
3563
  function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new Date()) {
2866
3564
  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);
3565
+ const start = prefill.start ? toLocalInput(prefill.start) : format7(startOfHour(addHours(now, 1)), LOCAL);
3566
+ let end = prefill.end ? toLocalInput(prefill.end) : format7(addHours(new Date(start), 1), LOCAL);
2869
3567
  if (prefill.allDay && prefill.end) end = inclusiveEnd(start, end);
2870
3568
  return {
2871
3569
  accountId,
@@ -2882,7 +3580,7 @@ function emptyForm(accounts, calendars, prefill = {}, now = /* @__PURE__ */ new
2882
3580
  };
2883
3581
  }
2884
3582
  var inclusiveEnd = (startLocal, endLocal) => {
2885
- const d = format6(addDays6(new Date(endLocal), -1), LOCAL);
3583
+ const d = format7(addDays6(new Date(endLocal), -1), LOCAL);
2886
3584
  return d.slice(0, 10) < startLocal.slice(0, 10) ? startLocal : d;
2887
3585
  };
2888
3586
  function formFromEvent(e) {
@@ -2907,53 +3605,53 @@ var withLoadedRepeat = (f, rule) => ({ ...f, repeat: rule, repeatWas: rule });
2907
3605
  var repeatChanged = (f) => f.repeatWas !== void 0 && !sameRecurrence(f.repeat, f.repeatWas, f.start);
2908
3606
  function setAllDay(f, allDay) {
2909
3607
  if (allDay || f.end > f.start) return { ...f, allDay };
2910
- return { ...f, allDay, end: format6(addHours(new Date(f.start), 1), LOCAL) };
3608
+ return { ...f, allDay, end: format7(addHours(new Date(f.start), 1), LOCAL) };
2911
3609
  }
2912
3610
  function moveStart(f, start) {
2913
3611
  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) };
3612
+ return { ...f, start, end: format7(end, LOCAL) };
2915
3613
  }
2916
- function times(f) {
3614
+ function times(f, locale) {
2917
3615
  if (f.allDay) {
2918
3616
  const start2 = f.start.slice(0, 10);
2919
3617
  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") };
3618
+ if (endIncl < start2) throw new Error(t(locale, "form.endBeforeStartDate"));
3619
+ return { allDay: true, start: start2, end: format7(addDays6(parseISO6(endIncl), 1), "yyyy-MM-dd") };
2922
3620
  }
2923
3621
  const start = fromLocalInput(f.start);
2924
3622
  const end = fromLocalInput(f.end);
2925
- if (end <= start) throw new Error("End must be after start");
3623
+ if (end <= start) throw new Error(t(locale, "form.endAfterStart"));
2926
3624
  return { allDay: false, start, end };
2927
3625
  }
2928
- function checkEmails(list) {
3626
+ function checkEmails(list, locale) {
2929
3627
  const bad = list.filter((e) => !isEmail(e));
2930
- if (bad.length) throw new Error(`Invalid email: ${bad.join(", ")}`);
3628
+ if (bad.length) throw new Error(t(locale, "form.invalidEmail", { list: bad.join(", ") }));
2931
3629
  return [...new Set(list.map((e) => e.toLowerCase()))];
2932
3630
  }
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);
3631
+ function formToInput(f, locale = "en") {
3632
+ if (!f.accountId) throw new Error(t(locale, "form.chooseAccount"));
3633
+ if (!f.calendarId) throw new Error(t(locale, "form.chooseCalendar"));
3634
+ const attendees = checkEmails(f.attendees, locale);
2937
3635
  return {
2938
3636
  accountId: f.accountId,
2939
3637
  calendarId: f.calendarId,
2940
- title: f.title.trim() || "New Event",
2941
- ...times(f),
3638
+ title: f.title.trim() || t(locale, "common.newEvent"),
3639
+ ...times(f, locale),
2942
3640
  location: f.location.trim() || void 0,
2943
3641
  description: f.description.trim() || void 0,
2944
3642
  ...attendees.length ? { attendees } : {},
2945
3643
  ...f.repeat ? { recurrence: f.repeat } : {}
2946
3644
  };
2947
3645
  }
2948
- function applyForm(e, f) {
2949
- const emails = checkEmails(f.attendees);
3646
+ function applyForm(e, f, locale = "en") {
3647
+ const emails = checkEmails(f.attendees, locale);
2950
3648
  const keep = e.attendees.filter((a) => a.self || a.organizer);
2951
3649
  const byEmail = new Map(e.attendees.map((a) => [a.email.toLowerCase(), a]));
2952
3650
  const invited = emails.filter((m) => !keep.some((a) => a.email.toLowerCase() === m)).map((m) => byEmail.get(m) ?? { email: m, status: "needsAction" });
2953
3651
  return {
2954
3652
  ...e,
2955
- title: f.title.trim() || "New Event",
2956
- ...times(f),
3653
+ title: f.title.trim() || t(locale, "common.newEvent"),
3654
+ ...times(f, locale),
2957
3655
  location: f.location.trim() || void 0,
2958
3656
  description: f.description.trim() || void 0,
2959
3657
  attendees: [...keep, ...invited],
@@ -3080,7 +3778,7 @@ import { Box as Box3, Text as Text3 } from "ink";
3080
3778
  import { addDays as addDays8, differenceInCalendarDays as differenceInCalendarDays2, differenceInMinutes as differenceInMinutes2, format as format9, isSameDay as isSameDay4, startOfDay as startOfDay3 } from "date-fns";
3081
3779
 
3082
3780
  // ../../packages/core/src/logic/details.ts
3083
- import { addDays as addDays7, format as format7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
3781
+ import { addDays as addDays7, isSameDay as isSameDay3, parseISO as parseISO7 } from "date-fns";
3084
3782
  var same = (a, b) => !!a && !!b && a.trim().toLowerCase() === b.trim().toLowerCase();
3085
3783
  function ownerLine(calendar, label, email) {
3086
3784
  return { calendar: same(calendar, label) || same(calendar, email) ? void 0 : calendar, label, email: same(email, label) ? void 0 : email };
@@ -3092,15 +3790,17 @@ function canEdit(e, account, calendar) {
3092
3790
  }
3093
3791
  var at2 = (iso2, allDay) => (allDay ? parseISO7(iso2) : new Date(iso2)).getTime();
3094
3792
  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) {
3793
+ function formatWhen(e, locale = "en") {
3794
+ const f = (d, p) => fmt(locale, d, p);
3096
3795
  if (e.allDay) {
3097
3796
  const s2 = parseISO7(e.start);
3098
3797
  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`;
3798
+ const allDay = t(locale, "when.allDay");
3799
+ return last > s2 ? `${f(s2, "EEE, d MMM")} \u2013 ${f(last, "EEE, d MMM")} \xB7 ${allDay}` : `${f(s2, "EEE, d MMM")} \xB7 ${allDay}`;
3100
3800
  }
3101
3801
  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")}`;
3802
+ const end = new Date(e.end);
3803
+ 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
3804
  }
3105
3805
  var STATUS_ICON = {
3106
3806
  accepted: "\u2713",
@@ -3108,31 +3808,106 @@ var STATUS_ICON = {
3108
3808
  declined: "\u2715",
3109
3809
  needsAction: "\u2022"
3110
3810
  };
3811
+ function linkify(text2) {
3812
+ const out = [];
3813
+ const plain = (t2) => {
3814
+ if (t2) out.push({ text: t2 });
3815
+ };
3816
+ let i = 0;
3817
+ for (const m of text2.matchAll(/https?:\/\/[^\s<>"]+/gi)) {
3818
+ const url = m[0].replace(/[.,;:!?)\]}>'"]+$/, "");
3819
+ plain(text2.slice(i, m.index));
3820
+ out.push({ text: url, href: url });
3821
+ i = m.index + url.length;
3822
+ }
3823
+ plain(text2.slice(i));
3824
+ return out;
3825
+ }
3111
3826
  var cleanNotes = (s = "") => s.replace(/^@color(hex)?:.*$\n?/gim, "").trim();
3827
+ var HTML_TAG = /<\/?(a|b|br|p|div|span|i|u|s|em|strong|del|ul|ol|li|h[1-6]|blockquote|pre|code|hr|table|thead|tbody|tr|td|th|font|img|sub|sup)\b[^>]*>/i;
3828
+ var isHtml = (s) => HTML_TAG.test(s);
3829
+ var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
3830
+ var decode = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e) => {
3831
+ if (e[0] !== "#") return ENTITIES[e.toLowerCase()] ?? m;
3832
+ const n = e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
3833
+ return n > 0 && n <= 1114111 ? String.fromCodePoint(n) : m;
3834
+ });
3835
+ function htmlToText(html) {
3836
+ return decode(
3837
+ html.replace(/<(script|style)\b[\s\S]*?<\/\1\s*>/gi, "").replace(/<a\b[^>]*?\bhref\s*=\s*["']?([^"'\s>]+)[^>]*>([\s\S]*?)<\/a\s*>/gi, (_, href, inner) => {
3838
+ const text2 = inner.replace(/<[^>]*>/g, "").trim();
3839
+ const url = decode(href);
3840
+ return !text2 || decode(text2) === url ? url : `${text2} (${url})`;
3841
+ }).replace(/<br\s*\/?>/gi, "\n").replace(/<li\b[^>]*>/gi, "\n\u2022 ").replace(/<\/(p|div|h[1-6]|tr|blockquote|pre|ul|ol)\s*>/gi, "\n").replace(/<[^>]*>/g, "")
3842
+ ).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
3843
+ }
3112
3844
 
3113
3845
  // ../../packages/core/src/logic/meeting.ts
3114
3846
  function meetingUrl(location) {
3115
3847
  return location?.match(/https?:\/\/[^\s<>"]+/i)?.[0].replace(/[.,;:!?)\]}'>]+$/, "");
3116
3848
  }
3117
- function place(location = "") {
3118
- const rest = location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3119
- return rest || (meetingUrl(location) ? "video call" : "");
3849
+ function eventMeetingUrl(e) {
3850
+ return e.conferenceUrl ?? meetingUrl(e.location);
3851
+ }
3852
+ function locationText(location = "") {
3853
+ return location.replace(/https?:\/\/[^\s<>"]+/gi, "").replace(/^[\s/|,·-]+|[\s/|,·-]+$/g, "");
3854
+ }
3855
+ function place(location = "", locale = "en") {
3856
+ return locationText(location) || (meetingUrl(location) ? t(locale, "meeting.videoCall") : "");
3857
+ }
3858
+ function eventPlace(e, locale = "en") {
3859
+ return place(e.location, locale) || (e.conferenceUrl ? t(locale, "meeting.videoCall") : "");
3860
+ }
3861
+ var host = (url) => {
3862
+ try {
3863
+ return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
3864
+ } catch {
3865
+ return "";
3866
+ }
3867
+ };
3868
+ var is = (h, domain) => h === domain || h.endsWith(`.${domain}`);
3869
+ function linkKind(url) {
3870
+ const h = host(url);
3871
+ const path = url.replace(/^https?:\/\/[^/]+/i, "").toLowerCase();
3872
+ if (h === "meet.google.com") return "meet";
3873
+ if (is(h, "zoom.us") || is(h, "zoom.com") || is(h, "zoomgov.com")) return "zoom";
3874
+ if (h === "teams.microsoft.com" || h === "teams.live.com") return "teams";
3875
+ 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"))
3876
+ return "map";
3877
+ 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"))
3878
+ return "video";
3879
+ return "link";
3880
+ }
3881
+ function linkLabel(url, kind = linkKind(url), locale = "en") {
3882
+ if (kind === "link") return host(url) || url;
3883
+ return t(locale, `link.${kind}`, kind === "video" ? { host: host(url) } : void 0);
3884
+ }
3885
+ function eventLinks(e) {
3886
+ const urls = [e.conferenceUrl, ...linkify(e.location ?? "").map((p) => p.href)];
3887
+ const seen = /* @__PURE__ */ new Set();
3888
+ const out = [];
3889
+ for (const url of urls) {
3890
+ if (!url || seen.has(url)) continue;
3891
+ seen.add(url);
3892
+ out.push({ url, kind: linkKind(url) });
3893
+ }
3894
+ return out;
3120
3895
  }
3121
3896
 
3122
3897
  // ../../packages/core/src/logic/status.ts
3123
3898
  import { differenceInMinutes, format as format8 } from "date-fns";
3124
3899
  function pickNowNext(events, now) {
3125
- const t = now.getTime();
3900
+ const t2 = now.getTime();
3126
3901
  const timed = events.filter((e) => !e.allDay && e.myStatus !== "declined").sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
3127
3902
  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)
3903
+ current: timed.filter((e) => Date.parse(e.start) <= t2 && t2 < Date.parse(e.end)),
3904
+ next: timed.find((e) => Date.parse(e.start) > t2)
3130
3905
  };
3131
3906
  }
3132
- function startsLabel(start, now) {
3907
+ function startsLabel(start, now, locale = "en") {
3133
3908
  const s = new Date(start);
3134
3909
  const m = differenceInMinutes(s, now, { roundingMethod: "ceil" });
3135
- return m <= 60 ? `in ${m}m` : `at ${format8(s, "HH:mm")}`;
3910
+ return m <= 60 ? t(locale, "status.inMinutes", { n: m }) : t(locale, "status.at", { time: format8(s, "HH:mm") });
3136
3911
  }
3137
3912
 
3138
3913
  // src/ui/screens/EventDetails.tsx
@@ -3158,7 +3933,7 @@ function EventDetails({ event: initial, onClose, onEdit }) {
3158
3933
  const calendar = calendars.find((c) => c.accountId === event.accountId && c.id === event.calendarId);
3159
3934
  const editable = canEdit(event, account, calendar);
3160
3935
  const recurring = !!event.recurringEventId;
3161
- const url = meetingUrl(event.location);
3936
+ const url = eventMeetingUrl(event);
3162
3937
  const now = useNow();
3163
3938
  const run = (label, fn) => {
3164
3939
  setBusy(label);
@@ -3231,10 +4006,14 @@ function relative(e, now) {
3231
4006
  return +start - +now < 24 * 36e5 ? startsLabel(e.start, now) : "";
3232
4007
  }
3233
4008
  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(", ");
4009
+ var LINK_GLYPH = { meet: "\u25B6", zoom: "\u25B6", teams: "\u25B6", video: "\u25B6", map: "\u2316", link: "\u2197" };
4010
+ var NAMED = /* @__PURE__ */ new Set(["meet", "zoom", "teams"]);
3234
4011
  var ATTENDEES_SHOWN = 5;
3235
4012
  function EventInfo({ event, account, calendar, now, showAll, onToggleAll }) {
3236
- const url = meetingUrl(event.location);
3237
- const notes = cleanNotes(event.description);
4013
+ const links = eventLinks(event);
4014
+ const where = locationText(event.location);
4015
+ const raw = cleanNotes(event.description);
4016
+ const notes = isHtml(raw) ? htmlToText(raw) : raw;
3238
4017
  const owner = ownerLine(calendar?.name ?? "Calendar", account?.label ?? event.accountId, account?.email);
3239
4018
  const when2 = [duration(event), relative(event, now)].filter(Boolean).join(" \xB7 ");
3240
4019
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
@@ -3256,11 +4035,18 @@ function EventInfo({ event, account, calendar, now, showAll, onToggleAll }) {
3256
4035
  owner.email && ` \xB7 ${owner.email}`,
3257
4036
  calendar?.readOnly && " \xB7 read-only"
3258
4037
  ] }),
3259
- event.location && /* @__PURE__ */ jsxs2(Text2, { children: [
4038
+ where && /* @__PURE__ */ jsxs2(Text2, { children: [
3260
4039
  "Location: ",
3261
- event.location
4040
+ where
3262
4041
  ] }),
3263
- url && url !== event.location && /* @__PURE__ */ jsx2(Clickable, { onClick: () => openUrl2(url), children: /* @__PURE__ */ jsx2(Text2, { color: C.cyan, underline: true, children: url }) }),
4042
+ links.map((l) => /* @__PURE__ */ jsx2(Clickable, { onClick: () => openUrl2(l.url), children: /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
4043
+ /* @__PURE__ */ jsxs2(Text2, { color: l.kind === "map" || l.kind === "link" ? C.muted : C.green, children: [
4044
+ LINK_GLYPH[l.kind],
4045
+ " ",
4046
+ NAMED.has(l.kind) ? `${linkLabel(l.url, l.kind)} ` : ""
4047
+ ] }),
4048
+ /* @__PURE__ */ jsx2(Text2, { color: C.cyan, underline: true, children: l.url })
4049
+ ] }) }, l.url)),
3264
4050
  event.organizer && /* @__PURE__ */ jsxs2(Text2, { children: [
3265
4051
  "Organizer: ",
3266
4052
  event.organizer.name ? `${event.organizer.name} <${event.organizer.email}>` : event.organizer.email
@@ -3350,10 +4136,10 @@ function busyMinutes(list, day) {
3350
4136
  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
4137
  let total = 0;
3352
4138
  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;
4139
+ for (const [s, t2] of iv) {
4140
+ if (t2 <= end) continue;
4141
+ total += t2 - Math.max(s, end);
4142
+ end = t2;
3357
4143
  }
3358
4144
  return Math.round(total / 6e4);
3359
4145
  }
@@ -3490,7 +4276,7 @@ function Agenda({ events, date, now, selectedKey, width, height, onSelect, onOpe
3490
4276
  const details = [{ text: who, color: C.muted }];
3491
4277
  if (e.recurringEventId) details.push({ text: " \u21BB repeats", color: C.muted });
3492
4278
  if (overlaps(e, eventsOnDay(events, r.day))) details.push({ text: " \u26A0 overlaps", color: C.yellow });
3493
- const where = place(e.location);
4279
+ const where = eventPlace(e);
3494
4280
  if (where) details.push({ text: ` ${where}`, color: C.cyan });
3495
4281
  if (e.attendees.length) details.push({ text: ` ${e.attendees.length} ${e.attendees.length === 1 ? "person" : "people"}`, color: C.muted });
3496
4282
  const row2 = [
@@ -3659,7 +4445,7 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3659
4445
  const { start, end } = eventBounds(e);
3660
4446
  const from = isSameDay6(start, day) ? format11(start, "HH:mm") : "\u2026";
3661
4447
  const title2 = `${rsvpMark(e)}${e.title || "(no title)"}`;
3662
- const where = place(e.location);
4448
+ const where = eventPlace(e);
3663
4449
  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
4450
  const text2 = lines[h - first] ?? "";
3665
4451
  const inner = p.col < p.cols - 1 && w > 1 ? w - 1 : w;
@@ -3732,8 +4518,8 @@ function TimeGrid({ days, events, now, cursor, selectedKey, width, height, onSel
3732
4518
  // src/ui/views/Day.tsx
3733
4519
  import { jsx as jsx6 } from "react/jsx-runtime";
3734
4520
  function useDays(date, n) {
3735
- const t = startOfDay5(date).getTime();
3736
- return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t, i)), [t, n]);
4521
+ const t2 = startOfDay5(date).getTime();
4522
+ return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(t2, i)), [t2, n]);
3737
4523
  }
3738
4524
  var Day = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 1) });
3739
4525
  var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date, 2) });
@@ -3742,8 +4528,8 @@ var TwoDay = (p) => /* @__PURE__ */ jsx6(TimeGrid, { ...p, days: useDays(p.date,
3742
4528
  import { useMemo as useMemo4 } from "react";
3743
4529
  import { jsx as jsx7 } from "react/jsx-runtime";
3744
4530
  function Week(p) {
3745
- const t = p.date.getTime();
3746
- const days = useMemo4(() => viewDays("week", new Date(t)), [t]);
4531
+ const t2 = p.date.getTime();
4532
+ const days = useMemo4(() => viewDays("week", new Date(t2)), [t2]);
3747
4533
  return /* @__PURE__ */ jsx7(TimeGrid, { ...p, days });
3748
4534
  }
3749
4535
 
@@ -4158,8 +4944,8 @@ var FRAMES = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
4158
4944
  function Spinner() {
4159
4945
  const [i, setI] = useState5(0);
4160
4946
  useEffect4(() => {
4161
- const t = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
4162
- return () => clearInterval(t);
4947
+ const t2 = setInterval(() => setI((n) => (n + 1) % FRAMES.length), 100);
4948
+ return () => clearInterval(t2);
4163
4949
  }, []);
4164
4950
  return /* @__PURE__ */ jsx10(Text8, { color: C.accent, children: FRAMES[i] });
4165
4951
  }
@@ -4616,7 +5402,8 @@ function Accounts({ onClose }) {
4616
5402
  button("g", "add Google"),
4617
5403
  button("a", "add CalDAV"),
4618
5404
  /* @__PURE__ */ jsx12(Button, { k: "esc", label: "close", onPress: onClose })
4619
- ] })
5405
+ ] }),
5406
+ mode.kind === "list" && /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { color: C.muted, children: "Made in Ukraine" }) })
4620
5407
  ] });
4621
5408
  }
4622
5409
  var loopbackPort = (authUrl) => {