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