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.
- package/dist/cli.js +1687 -900
- 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.
|
|
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: "
|
|
233
|
-
function noteText(notes, account, now = /* @__PURE__ */ new Date()) {
|
|
234
|
-
if (notes.length > 3) return [{ title:
|
|
235
|
-
return notes.map(({ kind, event: e }) => ({
|
|
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 `${
|
|
240
|
-
return `${isSameDay(s, now) ? "
|
|
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
|
|
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
|
|
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(`${
|
|
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/
|
|
498
|
-
|
|
499
|
-
var
|
|
500
|
-
var
|
|
501
|
-
var
|
|
502
|
-
var
|
|
503
|
-
var
|
|
504
|
-
var
|
|
505
|
-
var
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
var
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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(
|
|
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 ?
|
|
1069
|
-
const e = first.allDay ?
|
|
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
|
|
2188
|
+
const t2 = (iso2) => parseISO4(iso2).getTime();
|
|
1091
2189
|
return structuredClone(
|
|
1092
|
-
this.events.filter((e) => e.calendarId === calendarId &&
|
|
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) =>
|
|
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 = (
|
|
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,
|
|
2467
|
+
function setTime(vevent, name, t2) {
|
|
1367
2468
|
vevent.removeAllProperties(name);
|
|
1368
|
-
vevent.addPropertyWithValue(name,
|
|
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 = (
|
|
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((
|
|
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
|
|
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", (
|
|
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
|
|
1524
|
-
return !!
|
|
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,
|
|
2672
|
+
function timeProp(name, t2) {
|
|
1572
2673
|
const p = new ICAL.Property(name);
|
|
1573
|
-
p.setValue(
|
|
1574
|
-
const tzid =
|
|
1575
|
-
if (!
|
|
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,
|
|
2679
|
+
function replaceTime(v, name, t2) {
|
|
1579
2680
|
v.removeAllProperties(name);
|
|
1580
|
-
return timeProp(name,
|
|
2681
|
+
return timeProp(name, t2);
|
|
1581
2682
|
}
|
|
1582
2683
|
var dtstart = (v) => v.getFirstPropertyValue("dtstart");
|
|
1583
|
-
var dateOf = (
|
|
1584
|
-
var
|
|
1585
|
-
function addDays3(
|
|
1586
|
-
const x =
|
|
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,
|
|
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 = (
|
|
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 =
|
|
1603
|
-
return (
|
|
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 (
|
|
1608
|
-
const x = local(
|
|
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 =
|
|
1614
|
-
return (
|
|
1615
|
-
const d = addDays3(dateOf(
|
|
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
|
|
1632
|
-
if (
|
|
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
|
|
1691
|
-
if (
|
|
2791
|
+
const t2 = v.getFirstPropertyValue("recurrence-id");
|
|
2792
|
+
if (t2 && t2.toJSDate().getTime() < cut) root.removeSubcomponent(v);
|
|
1692
2793
|
}
|
|
1693
|
-
const later = (
|
|
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
|
|
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
|
|
1737
|
-
if (
|
|
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
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
}
|
|
1963
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1968
|
-
return typeof payload.email === "string" ? payload.email : void 0;
|
|
2878
|
+
new URL(serverUrl);
|
|
1969
2879
|
} catch {
|
|
1970
|
-
|
|
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
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
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
|
-
|
|
2058
|
-
const
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
-
|
|
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
|
|
2198
|
-
|
|
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
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
|
2235
|
-
const
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
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
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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
|
|
2269
|
-
const
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
-
|
|
2287
|
-
|
|
2288
|
-
const
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
|
2299
|
-
const
|
|
2300
|
-
if (
|
|
2301
|
-
const
|
|
2302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2321
|
-
|
|
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
|
|
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 `${
|
|
3449
|
+
return `${fmt(locale, a, head)} \u2013 ${fmt(locale, b, "d MMM yyyy")}`;
|
|
2752
3450
|
}
|
|
2753
|
-
var ymd = (d) =>
|
|
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
|
|
2833
|
-
return () => clearInterval(
|
|
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
|
|
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) =>
|
|
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) :
|
|
2868
|
-
let end = prefill.end ? toLocalInput(prefill.end) :
|
|
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 =
|
|
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:
|
|
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:
|
|
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(
|
|
2921
|
-
return { allDay: true, start: start2, end:
|
|
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(
|
|
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(
|
|
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(
|
|
2935
|
-
if (!f.calendarId) throw new Error(
|
|
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() ||
|
|
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() ||
|
|
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,
|
|
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
|
-
|
|
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
|
|
3103
|
-
return isSameDay3(s,
|
|
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
|
|
3118
|
-
|
|
3119
|
-
|
|
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
|
|
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) <=
|
|
3129
|
-
next: timed.find((e) => Date.parse(e.start) >
|
|
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 ?
|
|
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 =
|
|
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
|
|
3237
|
-
const
|
|
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
|
-
|
|
4038
|
+
where && /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
3260
4039
|
"Location: ",
|
|
3261
|
-
|
|
4040
|
+
where
|
|
3262
4041
|
] }),
|
|
3263
|
-
|
|
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,
|
|
3354
|
-
if (
|
|
3355
|
-
total +=
|
|
3356
|
-
end =
|
|
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 =
|
|
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 =
|
|
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
|
|
3736
|
-
return useMemo3(() => Array.from({ length: n }, (_, i) => addDays9(
|
|
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
|
|
3746
|
-
const days = useMemo4(() => viewDays("week", new Date(
|
|
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
|
|
4162
|
-
return () => clearInterval(
|
|
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) => {
|