From a5b7710fc8fe1ca5e2c3f67f955394bc95cfb165 Mon Sep 17 00:00:00 2001 From: albert Date: Sat, 8 Aug 2026 14:11:49 -0700 Subject: [PATCH 1/5] Refactor BusyMaxEmptyState to use Yaru icons and improve layout --- lib/src/app/busymax_design.dart | 50 +++++++---- .../presentation/schedule_empty_states.dart | 56 ++++++++++-- .../schedule_workspace_states_test.dart | 90 +++++++++++++++++++ 3 files changed, 172 insertions(+), 24 deletions(-) diff --git a/lib/src/app/busymax_design.dart b/lib/src/app/busymax_design.dart index 5411d1c..1f27c0f 100644 --- a/lib/src/app/busymax_design.dart +++ b/lib/src/app/busymax_design.dart @@ -3053,38 +3053,58 @@ class BusyMaxEmptyState extends StatelessWidget { @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final hasMessage = message != null && message!.isNotEmpty; return Center( - child: Padding( + child: SingleChildScrollView( padding: const EdgeInsets.all(BusyMaxSpacing.xl), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: YaruInfoBox( - yaruInfoType: YaruInfoType.information, - color: colorScheme.onSurfaceVariant, - icon: Icon(icon), - title: Text(title), + constraints: const BoxConstraints(maxWidth: 440), + child: Semantics( + container: true, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (message != null && message!.isNotEmpty) + ExcludeSemantics( + child: Icon( + icon, + size: 56, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: BusyMaxSpacing.lg), + Semantics( + header: true, + child: Text( + title, + textAlign: TextAlign.center, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + if (hasMessage) ...[ + const SizedBox(height: BusyMaxSpacing.sm), Text( message!, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), + ], if (actions.isNotEmpty) ...[ - if (message != null && message!.isNotEmpty) - const SizedBox(height: BusyMaxSpacing.lg), + const SizedBox(height: BusyMaxSpacing.lg), Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, spacing: BusyMaxSpacing.sm, runSpacing: BusyMaxSpacing.sm, children: actions, ), ], - if ((message == null || message!.isEmpty) && actions.isEmpty) - const SizedBox.shrink(), ], ), ), diff --git a/lib/src/features/schedule/presentation/schedule_empty_states.dart b/lib/src/features/schedule/presentation/schedule_empty_states.dart index 02f28da..e40351d 100644 --- a/lib/src/features/schedule/presentation/schedule_empty_states.dart +++ b/lib/src/features/schedule/presentation/schedule_empty_states.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; import '../../../app/busymax_design.dart'; import '../../../l10n/l10n.dart'; @@ -49,7 +50,7 @@ class ScheduleNoSourcesState extends StatelessWidget { @override Widget build(BuildContext context) { return BusyMaxEmptyState( - icon: Icons.calendar_month_outlined, + icon: YaruIcons.calendar, title: hasAccounts ? context.l10n.scheduleNoSources : context.l10n.scheduleSignInRequired, @@ -59,12 +60,18 @@ class ScheduleNoSourcesState extends StatelessWidget { actions: [ BusyMaxPushButton.suggested( onPressed: onOpenSettings, - child: Text(context.l10n.settings), + child: _ScheduleEmptyStateActionLabel( + icon: YaruIcons.settings, + label: context.l10n.settings, + ), ), if (onRefresh != null) BusyMaxPushButton.standard( onPressed: onRefresh, - child: Text(context.l10n.refresh), + child: _ScheduleEmptyStateActionLabel( + icon: YaruIcons.refresh, + label: context.l10n.refresh, + ), ), ], ); @@ -79,12 +86,15 @@ class ScheduleUnavailableState extends StatelessWidget { @override Widget build(BuildContext context) { return BusyMaxEmptyState( - icon: Icons.sync_problem_outlined, + icon: YaruIcons.sync_error, title: context.l10n.scheduleUnavailable, actions: [ BusyMaxPushButton.suggested( onPressed: onRetry, - child: Text(context.l10n.retry), + child: _ScheduleEmptyStateActionLabel( + icon: YaruIcons.refresh, + label: context.l10n.retry, + ), ), ], ); @@ -104,18 +114,24 @@ class ScheduleEmptyState extends StatelessWidget { @override Widget build(BuildContext context) { return BusyMaxEmptyState( - icon: Icons.event_available, + icon: YaruIcons.calendar_day, title: context.l10n.noEventsOrTasks, actions: [ if (onNewEvent != null) BusyMaxPushButton.standard( onPressed: onNewEvent, - child: Text(context.l10n.newEvent), + child: _ScheduleEmptyStateActionLabel( + icon: YaruIcons.calendar_new, + label: context.l10n.newEvent, + ), ), if (onNewTask != null) BusyMaxPushButton.standard( onPressed: onNewTask, - child: Text(context.l10n.newTask), + child: _ScheduleEmptyStateActionLabel( + icon: YaruIcons.task_list, + label: context.l10n.newTask, + ), ), ], ); @@ -128,9 +144,31 @@ class ScheduleSearchEmptyState extends StatelessWidget { @override Widget build(BuildContext context) { return BusyMaxEmptyState( - icon: Icons.search_off_outlined, + icon: YaruIcons.search, title: context.l10n.scheduleNoSearchResults, message: context.l10n.scheduleNoSearchResultsDescription, ); } } + +class _ScheduleEmptyStateActionLabel extends StatelessWidget { + const _ScheduleEmptyStateActionLabel({ + required this.icon, + required this.label, + }); + + final IconData icon; + final String label; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: BusyMaxSizes.iconSm), + const SizedBox(width: BusyMaxSpacing.sm), + Text(label), + ], + ); + } +} diff --git a/test/features/schedule/presentation/schedule_workspace_states_test.dart b/test/features/schedule/presentation/schedule_workspace_states_test.dart index 1371447..2b51685 100644 --- a/test/features/schedule/presentation/schedule_workspace_states_test.dart +++ b/test/features/schedule/presentation/schedule_workspace_states_test.dart @@ -18,6 +18,96 @@ import 'package:yaru/yaru.dart'; import '../../../test_localized_app.dart'; void main() { + testWidgets('no-source state is a compact centered status page', ( + tester, + ) async { + tester.view + ..devicePixelRatio = 1 + ..physicalSize = const Size(900, 600); + addTearDown(tester.view.reset); + var settingsOpened = false; + var refreshed = false; + + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: ScheduleNoSourcesState( + hasAccounts: true, + onOpenSettings: () => settingsOpened = true, + onRefresh: () => refreshed = true, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(YaruInfoBox), findsNothing); + expect(find.byIcon(YaruIcons.calendar), findsOneWidget); + expect(find.byIcon(YaruIcons.settings), findsOneWidget); + expect(find.byIcon(YaruIcons.refresh), findsOneWidget); + + final iconRect = tester.getRect(find.byIcon(YaruIcons.calendar)); + final actionRect = tester.getRect( + find.ancestor( + of: find.text('Settings'), + matching: find.byType(ElevatedButton), + ), + ); + final contentCenterY = (iconRect.top + actionRect.bottom) / 2; + expect(contentCenterY, closeTo(300, 2)); + expect(actionRect.bottom - iconRect.top, lessThan(280)); + + await tester.tap(find.text('Settings')); + await tester.tap(find.text('Refresh')); + expect(settingsOpened, isTrue); + expect(refreshed, isTrue); + }); + + testWidgets('empty agenda presents native peer creation actions', ( + tester, + ) async { + tester.view + ..devicePixelRatio = 1 + ..physicalSize = const Size(900, 600); + addTearDown(tester.view.reset); + var newEventOpened = false; + var newTaskOpened = false; + + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: ScheduleEmptyState( + onNewEvent: () => newEventOpened = true, + onNewTask: () => newTaskOpened = true, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(YaruInfoBox), findsNothing); + expect(find.byIcon(YaruIcons.calendar_day), findsOneWidget); + expect(find.byIcon(YaruIcons.calendar_new), findsOneWidget); + expect(find.byIcon(YaruIcons.task_list), findsOneWidget); + expect(find.byType(FilledButton), findsNWidgets(2)); + expect(find.byType(ElevatedButton), findsNothing); + + final eventButton = find.ancestor( + of: find.text('New event'), + matching: find.byType(FilledButton), + ); + final taskButton = find.ancestor( + of: find.text('New task'), + matching: find.byType(FilledButton), + ); + expect(tester.getCenter(eventButton).dy, tester.getCenter(taskButton).dy); + + await tester.tap(find.text('New event')); + await tester.tap(find.text('New task')); + expect(newEventOpened, isTrue); + expect(newTaskOpened, isTrue); + }); + testWidgets('schedule exposes a labeled loading state', (tester) async { final accounts = StreamController>(); addTearDown(accounts.close); From 0d73a83dfaeabfc730c4b7f36d2597b558e4b0ed Mon Sep 17 00:00:00 2001 From: albert Date: Sat, 8 Aug 2026 14:45:30 -0700 Subject: [PATCH 2/5] Rename variables for clarity and update localization strings in settings screen --- lib/l10n/app_ar.arb | 2 +- lib/l10n/app_de.arb | 2 +- lib/l10n/app_en.arb | 2 +- lib/l10n/app_es.arb | 2 +- lib/l10n/app_et.arb | 2 +- lib/l10n/app_fa.arb | 2 +- lib/l10n/app_fi.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_hi.arb | 2 +- lib/l10n/app_it.arb | 2 +- lib/l10n/app_ja.arb | 2 +- lib/l10n/app_ko.arb | 2 +- lib/l10n/app_pt.arb | 2 +- lib/l10n/app_ru.arb | 2 +- lib/l10n/app_vi.arb | 2 +- lib/l10n/app_zh.arb | 2 +- lib/l10n/app_zh_Hans.arb | 2 +- lib/l10n/app_zh_Hant.arb | 2 +- lib/l10n/generated/app_localizations.dart | 6 +++--- lib/l10n/generated/app_localizations_ar.dart | 2 +- lib/l10n/generated/app_localizations_de.dart | 2 +- lib/l10n/generated/app_localizations_en.dart | 2 +- lib/l10n/generated/app_localizations_es.dart | 2 +- lib/l10n/generated/app_localizations_et.dart | 2 +- lib/l10n/generated/app_localizations_fa.dart | 2 +- lib/l10n/generated/app_localizations_fi.dart | 2 +- lib/l10n/generated/app_localizations_fr.dart | 2 +- lib/l10n/generated/app_localizations_hi.dart | 2 +- lib/l10n/generated/app_localizations_it.dart | 2 +- lib/l10n/generated/app_localizations_ja.dart | 2 +- lib/l10n/generated/app_localizations_ko.dart | 2 +- lib/l10n/generated/app_localizations_pt.dart | 2 +- lib/l10n/generated/app_localizations_ru.dart | 2 +- lib/l10n/generated/app_localizations_vi.dart | 2 +- lib/l10n/generated/app_localizations_zh.dart | 6 +++--- .../settings/presentation/settings_screen.dart | 12 ++++++------ .../presentation/settings_screen_test.dart | 18 ++++++++++-------- 37 files changed, 55 insertions(+), 53 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 6a96363..d52c38a 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -186,7 +186,7 @@ "removeAccountAction": "إزالة الحساب", "removeAccountFailed": "تعذر إكمال إزالة الحساب. حاول مرة أخرى.", "accountRemovedGoogleRevokeFailed": "تمت إزالة الحساب من هذا الجهاز، لكن تعذر على BusyMax إلغاء الوصول إلى Google. يمكنك إلغاء الوصول من حسابك على Google.", - "newList": "قائمة جديدة", + "newTaskList": "قائمة مهام جديدة", "signInToViewTaskLists": "سجّل الدخول لعرض قوائم المهام.", "noTaskListsSynced": "لم تتم مزامنة أي قوائم مهام بعد.", "listActions": "إجراءات القائمة", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dd8a9bb..f7814fc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -190,7 +190,7 @@ "removeAccountAction": "Konto entfernen", "removeAccountFailed": "Das Konto konnte nicht vollständig entfernt werden. Versuchen Sie es erneut.", "accountRemovedGoogleRevokeFailed": "Das Konto wurde von diesem Gerät entfernt, aber BusyMax konnte den Google-Zugriff nicht widerrufen. Sie können ihn in Ihrem Google-Konto widerrufen.", - "newList": "Neue Liste", + "newTaskList": "Neue Aufgabenliste", "signInToViewTaskLists": "Melden Sie sich an, um Aufgabenlisten zu sehen.", "noTaskListsSynced": "Noch keine Aufgabenlisten synchronisiert.", "listActions": "Listenaktionen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index db12d05..d462d4f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -193,7 +193,7 @@ "removeAccountAction": "Remove account", "removeAccountFailed": "Could not finish removing the account. Try again.", "accountRemovedGoogleRevokeFailed": "The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.", - "newList": "New list", + "newTaskList": "New task list", "signInToViewTaskLists": "Sign in to view task lists.", "noTaskListsSynced": "No task lists synced yet.", "listActions": "List actions", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index cfd3fef..32e81a3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -190,7 +190,7 @@ "removeAccountAction": "Eliminar cuenta", "removeAccountFailed": "No se pudo terminar de eliminar la cuenta. Inténtalo de nuevo.", "accountRemovedGoogleRevokeFailed": "La cuenta se eliminó de este dispositivo, pero BusyMax no pudo revocar su acceso a tu cuenta de Google. Puedes revocarlo desde tu cuenta de Google.", - "newList": "Nueva lista", + "newTaskList": "Nueva lista de tareas", "signInToViewTaskLists": "Inicia sesión para ver las listas de tareas.", "noTaskListsSynced": "Aún no hay listas de tareas sincronizadas.", "listActions": "Acciones de lista", diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb index 6b952dc..d45de47 100644 --- a/lib/l10n/app_et.arb +++ b/lib/l10n/app_et.arb @@ -193,7 +193,7 @@ "removeAccountAction": "Eemalda konto", "removeAccountFailed": "Konto eemaldamist ei saanud lõpetada. Proovige uuesti.", "accountRemovedGoogleRevokeFailed": "Konto eemaldati sellest seadmest, kuid BusyMaxi juurdepääsu teie Google'i kontole ei saanud tühistada. Saate selle oma Google'i kontol käsitsi tühistada.", - "newList": "Uus loend", + "newTaskList": "Uus ülesandeloend", "signInToViewTaskLists": "Ülesandeloendite vaatamiseks logige sisse.", "noTaskListsSynced": "Ühtegi ülesandeloendit pole veel sünkroonitud.", "listActions": "Loendi toimingud", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 7caa2aa..ff614f5 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -186,7 +186,7 @@ "removeAccountAction": "حذف حساب", "removeAccountFailed": "حذف حساب کامل نشد. دوباره تلاش کنید.", "accountRemovedGoogleRevokeFailed": "حساب از این دستگاه حذف شد، اما BusyMax نتوانست دسترسی Google را لغو کند. می‌توانید آن را از حساب Google خود لغو کنید.", - "newList": "فهرست جدید", + "newTaskList": "فهرست کار جدید", "signInToViewTaskLists": "برای دیدن فهرست‌های کار وارد شوید.", "noTaskListsSynced": "هنوز هیچ فهرست کاری همگام نشده است.", "listActions": "عملیات فهرست", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 900bd8d..6ea730d 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -186,7 +186,7 @@ "removeAccountAction": "Poista tili", "removeAccountFailed": "Tilin poistamista ei voitu viimeistellä. Yritä uudelleen.", "accountRemovedGoogleRevokeFailed": "Tili poistettiin tältä laitteelta, mutta BusyMax ei voinut peruuttaa Google-käyttöoikeutta. Voit peruuttaa sen Google-tililtäsi.", - "newList": "Uusi luettelo", + "newTaskList": "Uusi tehtäväluettelo", "signInToViewTaskLists": "Kirjaudu sisään nähdäksesi tehtäväluettelot.", "noTaskListsSynced": "Tehtäväluetteloita ei ole vielä synkronoitu.", "listActions": "Luettelon toiminnot", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7d4d9a8..d0d61a0 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -190,7 +190,7 @@ "removeAccountAction": "Supprimer le compte", "removeAccountFailed": "Impossible de terminer la suppression du compte. Réessayez.", "accountRemovedGoogleRevokeFailed": "Le compte a été supprimé de cet appareil, mais BusyMax n’a pas pu révoquer son accès à votre compte Google. Vous pouvez révoquer cet accès depuis votre compte Google.", - "newList": "Nouvelle liste", + "newTaskList": "Nouvelle liste de tâches", "signInToViewTaskLists": "Connectez-vous pour voir les listes de tâches.", "noTaskListsSynced": "Aucune liste de tâches synchronisée.", "listActions": "Actions de liste", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index a809900..a1fd102 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -186,7 +186,7 @@ "removeAccountAction": "खाता हटाएँ", "removeAccountFailed": "खाता हटाना पूरा नहीं हो सका। फिर से कोशिश करें।", "accountRemovedGoogleRevokeFailed": "खाता इस डिवाइस से हटा दिया गया, लेकिन BusyMax की Google खाते तक पहुँच रद्द नहीं की जा सकी। आप यह पहुँच अपने Google खाते से रद्द कर सकते हैं।", - "newList": "नई सूची", + "newTaskList": "नई कार्य सूची", "signInToViewTaskLists": "कार्य सूचियाँ देखने के लिए साइन इन करें।", "noTaskListsSynced": "अभी तक कोई कार्य सूची सिंक नहीं हुई है।", "listActions": "सूची की कार्रवाइयाँ", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f486c22..909038b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -186,7 +186,7 @@ "removeAccountAction": "Rimuovi account", "removeAccountFailed": "Impossibile completare la rimozione dell’account. Riprova.", "accountRemovedGoogleRevokeFailed": "L’account è stato rimosso da questo dispositivo, ma BusyMax non è riuscito a revocare il proprio accesso a Google. Puoi revocarlo dal tuo account Google.", - "newList": "Nuovo elenco", + "newTaskList": "Nuovo elenco di attività", "signInToViewTaskLists": "Accedi per visualizzare gli elenchi di attività.", "noTaskListsSynced": "Nessun elenco di attività ancora sincronizzato.", "listActions": "Azioni dell’elenco", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 06b002e..039f92b 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -186,7 +186,7 @@ "removeAccountAction": "アカウントを削除", "removeAccountFailed": "アカウントの削除を完了できませんでした。もう一度お試しください。", "accountRemovedGoogleRevokeFailed": "アカウントはこのデバイスから削除されましたが、BusyMax は Google アカウントへのアクセス権を取り消せませんでした。Google アカウントの設定から取り消すことができます。", - "newList": "新しいリスト", + "newTaskList": "新しいタスクリスト", "signInToViewTaskLists": "タスクリストを表示するにはサインインしてください。", "noTaskListsSynced": "同期済みのタスクリストはまだありません。", "listActions": "リストの操作", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 183daaf..4ce3f7b 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -186,7 +186,7 @@ "removeAccountAction": "계정 삭제", "removeAccountFailed": "계정 삭제를 완료할 수 없습니다. 다시 시도하세요.", "accountRemovedGoogleRevokeFailed": "계정은 이 기기에서 삭제되었지만 BusyMax가 Google 계정 액세스 권한을 취소하지 못했습니다. Google 계정에서 직접 취소할 수 있습니다.", - "newList": "새 목록", + "newTaskList": "새 할 일 목록", "signInToViewTaskLists": "할 일 목록을 보려면 로그인하세요.", "noTaskListsSynced": "아직 동기화된 할 일 목록이 없습니다.", "listActions": "목록 작업", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index d330459..cc54962 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -186,7 +186,7 @@ "removeAccountAction": "Remover conta", "removeAccountFailed": "Não foi possível concluir a remoção da conta. Tente novamente.", "accountRemovedGoogleRevokeFailed": "A conta foi removida deste dispositivo, mas não foi possível revogar o acesso do BusyMax à sua conta Google. Pode revogar esse acesso na sua conta Google.", - "newList": "Nova lista", + "newTaskList": "Nova lista de tarefas", "signInToViewTaskLists": "Inicie sessão para ver as listas de tarefas.", "noTaskListsSynced": "Ainda não há listas de tarefas sincronizadas.", "listActions": "Ações da lista", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 6184aa3..cc5400d 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -186,7 +186,7 @@ "removeAccountAction": "Удалить аккаунт", "removeAccountFailed": "Не удалось завершить удаление аккаунта. Повторите попытку.", "accountRemovedGoogleRevokeFailed": "Аккаунт удалён с этого устройства, но отозвать доступ BusyMax к Google не удалось. Вы можете отозвать доступ в аккаунте Google.", - "newList": "Новый список", + "newTaskList": "Новый список задач", "signInToViewTaskLists": "Войдите, чтобы просмотреть списки задач.", "noTaskListsSynced": "Синхронизированных списков задач пока нет.", "listActions": "Действия со списком", diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 1a0d6a5..44e314f 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -186,7 +186,7 @@ "removeAccountAction": "Xóa tài khoản", "removeAccountFailed": "Không thể hoàn tất việc xóa tài khoản. Hãy thử lại.", "accountRemovedGoogleRevokeFailed": "Tài khoản đã bị xóa khỏi thiết bị này, nhưng BusyMax không thể thu hồi quyền truy cập vào tài khoản Google. Bạn có thể thu hồi quyền trong phần cài đặt Tài khoản Google.", - "newList": "Danh sách mới", + "newTaskList": "Danh sách công việc mới", "signInToViewTaskLists": "Đăng nhập để xem danh sách công việc.", "noTaskListsSynced": "Chưa có danh sách công việc nào được đồng bộ.", "listActions": "Thao tác với danh sách", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 71eb8a8..13a5ab4 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -186,7 +186,7 @@ "removeAccountAction": "移除帐户", "removeAccountFailed": "无法完成帐户移除。请重试。", "accountRemovedGoogleRevokeFailed": "该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。", - "newList": "新建列表", + "newTaskList": "新建任务列表", "signInToViewTaskLists": "登录以查看任务列表。", "noTaskListsSynced": "尚未同步任何任务列表。", "listActions": "列表操作", diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index 75338a1..50e7461 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -186,7 +186,7 @@ "removeAccountAction": "移除帐户", "removeAccountFailed": "无法完成帐户移除。请重试。", "accountRemovedGoogleRevokeFailed": "该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。", - "newList": "新建列表", + "newTaskList": "新建任务列表", "signInToViewTaskLists": "登录以查看任务列表。", "noTaskListsSynced": "尚未同步任何任务列表。", "listActions": "列表操作", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 778d8cc..bb07de0 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -186,7 +186,7 @@ "removeAccountAction": "移除帳戶", "removeAccountFailed": "無法完成帳戶移除。請再試一次。", "accountRemovedGoogleRevokeFailed": "該帳戶已從此裝置移除,但無法撤銷 BusyMax 對您的 Google 帳戶的存取權。您可以在 Google 帳戶中手動撤銷該權限。", - "newList": "新增清單", + "newTaskList": "新增待辦清單", "signInToViewTaskLists": "登入以查看待辦清單。", "noTaskListsSynced": "尚未同步任何待辦清單。", "listActions": "清單動作", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 77b8b4c..54ff62e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1248,11 +1248,11 @@ abstract class AppLocalizations { /// **'The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.'** String get accountRemovedGoogleRevokeFailed; - /// No description provided for @newList. + /// No description provided for @newTaskList. /// /// In en, this message translates to: - /// **'New list'** - String get newList; + /// **'New task list'** + String get newTaskList; /// No description provided for @signInToViewTaskLists. /// diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index f65376c..d403251 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -644,7 +644,7 @@ class AppLocalizationsAr extends AppLocalizations { 'تمت إزالة الحساب من هذا الجهاز، لكن تعذر على BusyMax إلغاء الوصول إلى Google. يمكنك إلغاء الوصول من حسابك على Google.'; @override - String get newList => 'قائمة جديدة'; + String get newTaskList => 'قائمة مهام جديدة'; @override String get signInToViewTaskLists => 'سجّل الدخول لعرض قوائم المهام.'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 0060526..f5f8a29 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -635,7 +635,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Das Konto wurde von diesem Gerät entfernt, aber BusyMax konnte den Google-Zugriff nicht widerrufen. Sie können ihn in Ihrem Google-Konto widerrufen.'; @override - String get newList => 'Neue Liste'; + String get newTaskList => 'Neue Aufgabenliste'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index e858e70..308a1a5 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -631,7 +631,7 @@ class AppLocalizationsEn extends AppLocalizations { 'The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.'; @override - String get newList => 'New list'; + String get newTaskList => 'New task list'; @override String get signInToViewTaskLists => 'Sign in to view task lists.'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 6a0da5f..d15f0b3 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -637,7 +637,7 @@ class AppLocalizationsEs extends AppLocalizations { 'La cuenta se eliminó de este dispositivo, pero BusyMax no pudo revocar su acceso a tu cuenta de Google. Puedes revocarlo desde tu cuenta de Google.'; @override - String get newList => 'Nueva lista'; + String get newTaskList => 'Nueva lista de tareas'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart index cba33c2..efc461f 100644 --- a/lib/l10n/generated/app_localizations_et.dart +++ b/lib/l10n/generated/app_localizations_et.dart @@ -633,7 +633,7 @@ class AppLocalizationsEt extends AppLocalizations { 'Konto eemaldati sellest seadmest, kuid BusyMaxi juurdepääsu teie Google\'i kontole ei saanud tühistada. Saate selle oma Google\'i kontol käsitsi tühistada.'; @override - String get newList => 'Uus loend'; + String get newTaskList => 'Uus ülesandeloend'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index 447985d..abb3d3d 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -651,7 +651,7 @@ class AppLocalizationsFa extends AppLocalizations { 'حساب از این دستگاه حذف شد، اما BusyMax نتوانست دسترسی Google را لغو کند. می‌توانید آن را از حساب Google خود لغو کنید.'; @override - String get newList => 'فهرست جدید'; + String get newTaskList => 'فهرست کار جدید'; @override String get signInToViewTaskLists => 'برای دیدن فهرست‌های کار وارد شوید.'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 5f5a3ee..14379a2 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -636,7 +636,7 @@ class AppLocalizationsFi extends AppLocalizations { 'Tili poistettiin tältä laitteelta, mutta BusyMax ei voinut peruuttaa Google-käyttöoikeutta. Voit peruuttaa sen Google-tililtäsi.'; @override - String get newList => 'Uusi luettelo'; + String get newTaskList => 'Uusi tehtäväluettelo'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 17b96a0..0a38e0c 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -635,7 +635,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Le compte a été supprimé de cet appareil, mais BusyMax n’a pas pu révoquer son accès à votre compte Google. Vous pouvez révoquer cet accès depuis votre compte Google.'; @override - String get newList => 'Nouvelle liste'; + String get newTaskList => 'Nouvelle liste de tâches'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 71cb781..9ae2566 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -635,7 +635,7 @@ class AppLocalizationsHi extends AppLocalizations { 'खाता इस डिवाइस से हटा दिया गया, लेकिन BusyMax की Google खाते तक पहुँच रद्द नहीं की जा सकी। आप यह पहुँच अपने Google खाते से रद्द कर सकते हैं।'; @override - String get newList => 'नई सूची'; + String get newTaskList => 'नई कार्य सूची'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 426438b..725f326 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -637,7 +637,7 @@ class AppLocalizationsIt extends AppLocalizations { 'L’account è stato rimosso da questo dispositivo, ma BusyMax non è riuscito a revocare il proprio accesso a Google. Puoi revocarlo dal tuo account Google.'; @override - String get newList => 'Nuovo elenco'; + String get newTaskList => 'Nuovo elenco di attività'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 1da583e..9ab1999 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -619,7 +619,7 @@ class AppLocalizationsJa extends AppLocalizations { 'アカウントはこのデバイスから削除されましたが、BusyMax は Google アカウントへのアクセス権を取り消せませんでした。Google アカウントの設定から取り消すことができます。'; @override - String get newList => '新しいリスト'; + String get newTaskList => '新しいタスクリスト'; @override String get signInToViewTaskLists => 'タスクリストを表示するにはサインインしてください。'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index dee60a8..adf823f 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -619,7 +619,7 @@ class AppLocalizationsKo extends AppLocalizations { '계정은 이 기기에서 삭제되었지만 BusyMax가 Google 계정 액세스 권한을 취소하지 못했습니다. Google 계정에서 직접 취소할 수 있습니다.'; @override - String get newList => '새 목록'; + String get newTaskList => '새 할 일 목록'; @override String get signInToViewTaskLists => '할 일 목록을 보려면 로그인하세요.'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 14bbdea..052f856 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -637,7 +637,7 @@ class AppLocalizationsPt extends AppLocalizations { 'A conta foi removida deste dispositivo, mas não foi possível revogar o acesso do BusyMax à sua conta Google. Pode revogar esse acesso na sua conta Google.'; @override - String get newList => 'Nova lista'; + String get newTaskList => 'Nova lista de tarefas'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 728ec35..7db883b 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -640,7 +640,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Аккаунт удалён с этого устройства, но отозвать доступ BusyMax к Google не удалось. Вы можете отозвать доступ в аккаунте Google.'; @override - String get newList => 'Новый список'; + String get newTaskList => 'Новый список задач'; @override String get signInToViewTaskLists => diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 0abb69c..4a14bf5 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -634,7 +634,7 @@ class AppLocalizationsVi extends AppLocalizations { 'Tài khoản đã bị xóa khỏi thiết bị này, nhưng BusyMax không thể thu hồi quyền truy cập vào tài khoản Google. Bạn có thể thu hồi quyền trong phần cài đặt Tài khoản Google.'; @override - String get newList => 'Danh sách mới'; + String get newTaskList => 'Danh sách công việc mới'; @override String get signInToViewTaskLists => 'Đăng nhập để xem danh sách công việc.'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 5d7f2bf..0e6292d 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -611,7 +611,7 @@ class AppLocalizationsZh extends AppLocalizations { '该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。'; @override - String get newList => '新建列表'; + String get newTaskList => '新建任务列表'; @override String get signInToViewTaskLists => '登录以查看任务列表。'; @@ -1819,7 +1819,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { '该帐户已从此设备移除,但无法撤销 BusyMax 对您的 Google 帐户的访问权限。您可以在 Google 帐户中手动撤销该权限。'; @override - String get newList => '新建列表'; + String get newTaskList => '新建任务列表'; @override String get signInToViewTaskLists => '登录以查看任务列表。'; @@ -3027,7 +3027,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { '該帳戶已從此裝置移除,但無法撤銷 BusyMax 對您的 Google 帳戶的存取權。您可以在 Google 帳戶中手動撤銷該權限。'; @override - String get newList => '新增清單'; + String get newTaskList => '新增待辦清單'; @override String get signInToViewTaskLists => '登入以查看待辦清單。'; diff --git a/lib/src/features/settings/presentation/settings_screen.dart b/lib/src/features/settings/presentation/settings_screen.dart index 7b11ee5..fb30170 100644 --- a/lib/src/features/settings/presentation/settings_screen.dart +++ b/lib/src/features/settings/presentation/settings_screen.dart @@ -811,7 +811,7 @@ Future _taskListTitleDialog( ) { return showBusyMaxTextPrompt( context, - title: context.l10n.newList, + title: context.l10n.newTaskList, label: context.l10n.title, actionLabel: context.l10n.create, headerBarService: headerBarService, @@ -912,7 +912,7 @@ class _AccountManagementCard extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; return BusyMaxGroupedList( - title: _providerLabel(context, account.provider), + title: _accountProviderLabel(context, account.provider), description: _accountIdentityLabel(context, account), filled: true, children: [ @@ -928,7 +928,7 @@ class _AccountManagementCard extends StatelessWidget { ) else ...[ BusyMaxActionRow( - title: l10n.newList, + title: l10n.newTaskList, leading: const Icon(YaruIcons.plus), onTap: removing ? null : onCreateTaskList, ), @@ -1028,9 +1028,9 @@ String _accountIdentityLabel(BuildContext context, AccountEntity account) { return context.l10n.signedInAccount; } -String _providerLabel(BuildContext context, TaskProvider provider) { +String _accountProviderLabel(BuildContext context, TaskProvider provider) { return switch (provider) { - TaskProvider.google => context.l10n.googleTasksProvider, - TaskProvider.microsoft => context.l10n.microsoftTodoProvider, + TaskProvider.google => context.l10n.googleProvider, + TaskProvider.microsoft => context.l10n.microsoftProvider, }; } diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index b3ef0d4..597afbb 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -258,7 +258,7 @@ void main() { expect(find.text(accountReconnectRequiredActionLabel), findsOneWidget); expect(find.text(accountReconnectRequiredSyncMessage), findsOneWidget); - expect(find.text('New list'), findsNothing); + expect(find.text('New task list'), findsNothing); expect(find.text('Remove account…'), findsOneWidget); await _openAccountRemovalDialog(tester); @@ -549,7 +549,7 @@ void main() { expect(second.state.scheduleDayEndMinute, 24 * 60); }); - testWidgets('Settings creates a new list for the account card', ( + testWidgets('Settings creates a new task list for the account card', ( tester, ) async { final googleLists = _FakeTaskListsRepository(); @@ -568,11 +568,11 @@ void main() { await _pumpSettings(tester, container); - final newListButtons = find.text('New list'); - expect(newListButtons, findsNWidgets(2)); + final newTaskListButtons = find.text('New task list'); + expect(newTaskListButtons, findsNWidgets(2)); - await tester.ensureVisible(newListButtons.at(1)); - await tester.tap(newListButtons.at(1)); + await tester.ensureVisible(newTaskListButtons.at(1)); + await tester.tap(newTaskListButtons.at(1)); await tester.pumpAndSettle(); final promptField = find.descendant( @@ -602,10 +602,12 @@ void main() { await _pumpSettings(tester, container); - expect(find.text('Google Tasks'), findsOneWidget); + expect(find.text('Google'), findsOneWidget); expect(find.text('Google User · google@example.com'), findsOneWidget); - expect(find.text('Microsoft To Do'), findsOneWidget); + expect(find.text('Microsoft'), findsOneWidget); expect(find.text('Microsoft User · microsoft@example.com'), findsOneWidget); + expect(find.text('Google Tasks'), findsNothing); + expect(find.text('Microsoft To Do'), findsNothing); expect(find.text('Current account'), findsNothing); expect(find.text('Switch account'), findsNothing); expect(find.text('Fluent UI'), findsNothing); From 04c97fde6792b171fcdce2ccd6f51f59aab0136a Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 10 Aug 2026 15:24:21 -0700 Subject: [PATCH 3/5] Add CalDAV providers and unified task support --- lib/l10n/app_ar.arb | 253 +- lib/l10n/app_de.arb | 277 +- lib/l10n/app_en.arb | 190 +- lib/l10n/app_es.arb | 277 +- lib/l10n/app_et.arb | 405 +- lib/l10n/app_fa.arb | 255 +- lib/l10n/app_fi.arb | 207 +- lib/l10n/app_fr.arb | 277 +- lib/l10n/app_hi.arb | 207 +- lib/l10n/app_it.arb | 207 +- lib/l10n/app_ja.arb | 207 +- lib/l10n/app_ko.arb | 207 +- lib/l10n/app_pt.arb | 207 +- lib/l10n/app_ru.arb | 207 +- lib/l10n/app_vi.arb | 207 +- lib/l10n/app_zh.arb | 207 +- lib/l10n/app_zh_Hans.arb | 207 +- lib/l10n/app_zh_Hant.arb | 207 +- lib/l10n/generated/app_localizations.dart | 938 +- lib/l10n/generated/app_localizations_ar.dart | 544 +- lib/l10n/generated/app_localizations_de.dart | 545 +- lib/l10n/generated/app_localizations_en.dart | 544 +- lib/l10n/generated/app_localizations_es.dart | 545 +- lib/l10n/generated/app_localizations_et.dart | 544 +- lib/l10n/generated/app_localizations_fa.dart | 544 +- lib/l10n/generated/app_localizations_fi.dart | 544 +- lib/l10n/generated/app_localizations_fr.dart | 544 +- lib/l10n/generated/app_localizations_hi.dart | 544 +- lib/l10n/generated/app_localizations_it.dart | 544 +- lib/l10n/generated/app_localizations_ja.dart | 543 +- lib/l10n/generated/app_localizations_ko.dart | 544 +- lib/l10n/generated/app_localizations_pt.dart | 544 +- lib/l10n/generated/app_localizations_ru.dart | 544 +- lib/l10n/generated/app_localizations_vi.dart | 545 +- lib/l10n/generated/app_localizations_zh.dart | 1978 +- lib/src/app/app_bootstrap.dart | 257 +- lib/src/app/busymax_dialogs.dart | 2 + .../calendar_providers/calendar_colors.dart | 4 +- .../calendar_providers/calendar_sync_dto.dart | 2 +- .../cloud_calendar_client.dart | 2 +- lib/src/config/build_config.dart | 7 +- .../oauth => core/auth}/oauth_models.dart | 1 + lib/src/core/logging/redacting_logger.dart | 101 +- .../portal_encrypted_secret_store.dart} | 227 +- lib/src/core/secrets/secret_store.dart | 516 + lib/src/dav/auth/dav_account_dialogs.dart | 230 + .../auth/dav_account_onboarding_service.dart | 468 + .../auth/nextcloud_app_password_revoker.dart | 58 + lib/src/dav/auth/nextcloud_login_flow_v2.dart | 460 + lib/src/dav/dav_errors.dart | 293 + lib/src/dav/dav_href.dart | 54 + lib/src/dav/dav_provider_profile.dart | 132 + .../dav/discovery/dav_discovery_models.dart | 118 + .../discovery/dav_discovery_repository.dart | 314 + .../dav/discovery/dav_discovery_service.dart | 715 + lib/src/dav/http/dav_http_transport.dart | 513 + lib/src/dav/ical/ical_document.dart | 782 + lib/src/dav/ical/ical_recurrence.dart | 979 + lib/src/dav/ical/ical_semantics.dart | 586 + lib/src/dav/ical/ical_task_alarm.dart | 400 + lib/src/dav/ical/ical_task_recurrence.dart | 485 + lib/src/dav/ical/ical_timezone.dart | 530 + .../dav_conditional_mutation_service.dart | 924 + .../dav/mutation/dav_conflict_analyzer.dart | 182 + .../dav/mutation/dav_conflict_repository.dart | 440 + lib/src/dav/mutation/dav_mutation_patch.dart | 675 + .../dav/mutation/dav_pending_operations.dart | 1552 + .../mutation/dav_projection_mutations.dart | 1526 + .../dav_task_list_mutation_service.dart | 575 + .../storage/dav_collection_capabilities.dart | 96 + .../dav/storage/dav_object_repository.dart | 1467 + .../dav/storage/dav_settings_repository.dart | 305 + lib/src/dav/sync/dav_account_sync_engine.dart | 569 + .../sync/dav_collection_remote_client.dart | 578 + lib/src/dav/sync/dav_sync_engine.dart | 404 + lib/src/dav/xml/dav_xml.dart | 423 + lib/src/db/app_database.dart | 7 +- lib/src/db/app_database.g.dart | 40218 +++++++++++----- lib/src/db/daos/task_lists_dao.dart | 49 +- lib/src/db/daos/tasks_dao.dart | 58 +- lib/src/db/migrations.dart | 562 +- lib/src/db/tables.dart | 291 +- lib/src/demo/demo_profile.dart | 6 +- lib/src/demo/demo_seed.dart | 6 +- .../accounts/data/accounts_repository.dart | 169 +- .../domain/account_connection_state.dart | 39 + .../features/auth/data/auth_repository.dart | 35 +- .../auth/presentation/sign_in_screen.dart | 160 +- .../calendar/data/calendar_repository.dart | 739 +- .../event_description_editor.dart | 4 +- .../calendar/presentation/event_editor.dart | 357 +- .../presentation/event_editor_draft.dart | 17 + .../notification_schedule_service.dart | 8 +- .../presentation/schedule_agenda_view.dart | 459 +- .../presentation/schedule_item_exporter.dart | 29 +- .../presentation/schedule_sidebar.dart | 244 +- .../presentation/schedule_task_chip.dart | 24 +- .../presentation/schedule_workspace.dart | 131 +- .../presentation/settings_screen.dart | 603 +- .../sync/account_sync_operations.dart | 46 + .../sync/calendar_pending_ops_replayer.dart | 6 +- .../features/sync/calendar_sync_engine.dart | 42 +- .../sync/pending_op_resolution_service.dart | 14 +- .../features/sync/pending_ops_replayer.dart | 251 +- lib/src/features/sync/sync_auth_error.dart | 2 +- lib/src/features/sync/sync_engine.dart | 72 +- .../data/task_lists_repository.dart | 62 +- .../features/tasks/data/tasks_repository.dart | 2551 +- .../tasks/domain/task_capabilities.dart | 243 + .../tasks/domain/task_checklist_item.dart | 56 + .../tasks/domain/task_remote_client.dart | 91 + .../tasks/domain/task_remote_error.dart | 46 + .../tasks/domain/task_remote_models.dart} | 46 +- .../presentation/ical_task_fields_editor.dart | 1573 + .../tasks/presentation/new_task_dialog.dart | 65 +- .../presentation/task_details_draft.dart | 295 +- .../presentation/task_details_editor.dart | 631 +- .../tasks/presentation/task_details_pane.dart | 325 +- .../google_calendar_api_client.dart | 4 +- .../google_calendar_mapper.dart | 6 +- .../api/google_tasks_api_client.dart | 61 +- .../api/google_tasks_api_error.dart | 18 +- .../http/authenticated_http_client.dart | 2 +- .../oauth/oauth_loopback_flow.dart | 2 +- lib/src/google_tasks/oauth/oauth_service.dart | 43 +- .../google_tasks/oauth/oauth_token_store.dart | 209 - .../microsoft_calendar_api_client.dart | 4 +- .../microsoft_calendar_mapper.dart | 8 +- .../api/microsoft_todo_api_client.dart | 81 +- .../api/microsoft_todo_api_models.dart | 53 + .../api/microsoft_todo_paths.dart | 13 + ...=> microsoft_todo_task_remote_client.dart} | 123 +- .../oauth/microsoft_oauth_service.dart | 37 +- lib/src/providers/account_authority.dart | 105 + lib/src/providers/busy_provider.dart | 84 + lib/src/providers/provider_capabilities.dart | 131 + lib/src/schedule/schedule_item.dart | 13 +- lib/src/schedule/schedule_projection.dart | 88 +- lib/src/schedule/schedule_repository.dart | 440 +- lib/src/task_providers/task_provider.dart | 97 - linux/io.busystack.busymax.metainfo.xml | 12 +- pubspec.lock | 4 +- pubspec.yaml | 4 +- snap/snapcraft.yaml | 15 +- test/app/about_dialog_test.dart | 13 +- test/app/app_bootstrap_provider_test.dart | 16 +- test/app/caldav_release_metadata_test.dart | 47 + test/app/native_ui_audit_test.dart | 4 +- test/config/build_config_test.dart | 18 + test/core/logging/redacting_logger_test.dart | 65 + .../apple_icloud_live_integration_test.dart | 531 + test/dav/auth/dav_account_dialogs_test.dart | 51 + .../dav_account_onboarding_service_test.dart | 682 + .../nextcloud_app_password_revoker_test.dart | 113 + .../auth/nextcloud_login_flow_v2_test.dart | 294 + test/dav/dav_errors_test.dart | 127 + test/dav/discovery/dav_discovery_test.dart | 343 + .../dav/fake_dav_server_integration_test.dart | 438 + test/dav/http/dav_http_transport_test.dart | 292 + test/dav/ical/ical_document_test.dart | 318 + test/dav/ical/ical_recurrence_test.dart | 336 + test/dav/ical/ical_task_alarm_test.dart | 160 + test/dav/ical/ical_task_recurrence_test.dart | 122 + ...dav_conditional_mutation_service_test.dart | 666 + .../dav/mutation/dav_mutation_patch_test.dart | 684 + .../mutation/dav_pending_operations_test.dart | 798 + ..._repository_mutation_integration_test.dart | 1305 + .../dav_task_list_mutation_service_test.dart | 336 + test/dav/nextcloud_live_integration_test.dart | 1436 + test/dav/nextcloud_login_flow_live_test.dart | 468 + test/dav/nextcloud_sharing_live_test.dart | 364 + .../storage/dav_object_repository_test.dart | 506 + test/dav/support/fake_dav_server.dart | 632 + .../sync/dav_account_sync_engine_test.dart | 417 + test/dav/sync/dav_sync_engine_test.dart | 578 + test/dav/xml/dav_xml_test.dart | 145 + test/db/app_database_test.dart | 199 +- test/demo/demo_profile_test.dart | 7 +- .../data/accounts_repository_test.dart | 57 + .../auth/data/auth_repository_test.dart | 52 +- .../auth/presentation/auth_routing_test.dart | 61 +- .../data/calendar_repository_test.dart | 40 +- .../presentation/event_editor_test.dart | 409 +- .../notification_schedule_service_test.dart | 49 +- .../notification_scheduler_test.dart | 12 +- .../schedule_sidebar_provider_test.dart | 146 + .../presentation/schedule_views_test.dart | 362 +- ...chedule_workspace_task_mutations_test.dart | 33 +- .../schedule/schedule_search_test.dart | 375 +- .../presentation/settings_screen_test.dart | 137 +- .../sync/calendar_event_clear_patch_test.dart | 27 +- .../calendar_pending_ops_replayer_test.dart | 36 +- .../sync/calendar_sync_engine_test.dart | 141 +- .../pending_mutation_sync_requester_test.dart | 14 +- .../pending_op_resolution_service_test.dart | 14 +- .../sync/pending_ops_replayer_test.dart | 256 +- test/features/sync/sync_engine_test.dart | 125 +- test/features/sync/sync_scheduler_test.dart | 8 +- .../data/task_lists_repository_test.dart | 87 + .../tasks/data/tasks_repository_test.dart | 189 + .../presentation/task_details_draft_test.dart | 51 +- .../presentation/task_details_pane_test.dart | 337 +- test/fixtures/schema_v5_production_like.sql | 48 + .../google_calendar_api_client_test.dart | 5 +- .../api/tasklists_patch_test.dart | 2 +- .../api/tasklists_update_test.dart | 2 +- test/google_tasks/api/tasks_insert_test.dart | 2 +- test/google_tasks/api/tasks_patch_test.dart | 2 +- test/google_tasks/api/tasks_update_test.dart | 2 +- .../http/authenticated_http_client_test.dart | 23 +- test/google_tasks/oauth/callback_test.dart | 2 +- .../oauth/loopback_flow_test.dart | 2 +- .../oauth/oauth_token_store_test.dart | 195 +- .../oauth/token_exchange_test.dart | 127 +- .../api/microsoft_todo_api_client_test.dart | 51 +- .../api/microsoft_todo_api_models_test.dart | 22 + ...crosoft_todo_task_remote_client_test.dart} | 133 +- .../oauth/microsoft_oauth_service_test.dart | 6 +- test/providers/busy_provider_test.dart | 143 + test/smoke_test.dart | 16 +- 220 files changed, 87639 insertions(+), 14203 deletions(-) rename lib/src/{google_tasks/oauth => core/auth}/oauth_models.dart (97%) rename lib/src/{google_tasks/oauth/portal_encrypted_oauth_token_store.dart => core/secrets/portal_encrypted_secret_store.dart} (68%) create mode 100644 lib/src/core/secrets/secret_store.dart create mode 100644 lib/src/dav/auth/dav_account_dialogs.dart create mode 100644 lib/src/dav/auth/dav_account_onboarding_service.dart create mode 100644 lib/src/dav/auth/nextcloud_app_password_revoker.dart create mode 100644 lib/src/dav/auth/nextcloud_login_flow_v2.dart create mode 100644 lib/src/dav/dav_errors.dart create mode 100644 lib/src/dav/dav_href.dart create mode 100644 lib/src/dav/dav_provider_profile.dart create mode 100644 lib/src/dav/discovery/dav_discovery_models.dart create mode 100644 lib/src/dav/discovery/dav_discovery_repository.dart create mode 100644 lib/src/dav/discovery/dav_discovery_service.dart create mode 100644 lib/src/dav/http/dav_http_transport.dart create mode 100644 lib/src/dav/ical/ical_document.dart create mode 100644 lib/src/dav/ical/ical_recurrence.dart create mode 100644 lib/src/dav/ical/ical_semantics.dart create mode 100644 lib/src/dav/ical/ical_task_alarm.dart create mode 100644 lib/src/dav/ical/ical_task_recurrence.dart create mode 100644 lib/src/dav/ical/ical_timezone.dart create mode 100644 lib/src/dav/mutation/dav_conditional_mutation_service.dart create mode 100644 lib/src/dav/mutation/dav_conflict_analyzer.dart create mode 100644 lib/src/dav/mutation/dav_conflict_repository.dart create mode 100644 lib/src/dav/mutation/dav_mutation_patch.dart create mode 100644 lib/src/dav/mutation/dav_pending_operations.dart create mode 100644 lib/src/dav/mutation/dav_projection_mutations.dart create mode 100644 lib/src/dav/mutation/dav_task_list_mutation_service.dart create mode 100644 lib/src/dav/storage/dav_collection_capabilities.dart create mode 100644 lib/src/dav/storage/dav_object_repository.dart create mode 100644 lib/src/dav/storage/dav_settings_repository.dart create mode 100644 lib/src/dav/sync/dav_account_sync_engine.dart create mode 100644 lib/src/dav/sync/dav_collection_remote_client.dart create mode 100644 lib/src/dav/sync/dav_sync_engine.dart create mode 100644 lib/src/dav/xml/dav_xml.dart create mode 100644 lib/src/features/accounts/domain/account_connection_state.dart create mode 100644 lib/src/features/tasks/domain/task_capabilities.dart create mode 100644 lib/src/features/tasks/domain/task_checklist_item.dart create mode 100644 lib/src/features/tasks/domain/task_remote_client.dart create mode 100644 lib/src/features/tasks/domain/task_remote_error.dart rename lib/src/{google_tasks/api/google_tasks_api_models.dart => features/tasks/domain/task_remote_models.dart} (87%) create mode 100644 lib/src/features/tasks/presentation/ical_task_fields_editor.dart delete mode 100644 lib/src/google_tasks/oauth/oauth_token_store.dart rename lib/src/microsoft_todo/api/{microsoft_todo_google_tasks_adapter.dart => microsoft_todo_task_remote_client.dart} (76%) create mode 100644 lib/src/providers/account_authority.dart create mode 100644 lib/src/providers/busy_provider.dart create mode 100644 lib/src/providers/provider_capabilities.dart delete mode 100644 lib/src/task_providers/task_provider.dart create mode 100644 test/app/caldav_release_metadata_test.dart create mode 100644 test/dav/apple_icloud_live_integration_test.dart create mode 100644 test/dav/auth/dav_account_dialogs_test.dart create mode 100644 test/dav/auth/dav_account_onboarding_service_test.dart create mode 100644 test/dav/auth/nextcloud_app_password_revoker_test.dart create mode 100644 test/dav/auth/nextcloud_login_flow_v2_test.dart create mode 100644 test/dav/dav_errors_test.dart create mode 100644 test/dav/discovery/dav_discovery_test.dart create mode 100644 test/dav/fake_dav_server_integration_test.dart create mode 100644 test/dav/http/dav_http_transport_test.dart create mode 100644 test/dav/ical/ical_document_test.dart create mode 100644 test/dav/ical/ical_recurrence_test.dart create mode 100644 test/dav/ical/ical_task_alarm_test.dart create mode 100644 test/dav/ical/ical_task_recurrence_test.dart create mode 100644 test/dav/mutation/dav_conditional_mutation_service_test.dart create mode 100644 test/dav/mutation/dav_mutation_patch_test.dart create mode 100644 test/dav/mutation/dav_pending_operations_test.dart create mode 100644 test/dav/mutation/dav_repository_mutation_integration_test.dart create mode 100644 test/dav/mutation/dav_task_list_mutation_service_test.dart create mode 100644 test/dav/nextcloud_live_integration_test.dart create mode 100644 test/dav/nextcloud_login_flow_live_test.dart create mode 100644 test/dav/nextcloud_sharing_live_test.dart create mode 100644 test/dav/storage/dav_object_repository_test.dart create mode 100644 test/dav/support/fake_dav_server.dart create mode 100644 test/dav/sync/dav_account_sync_engine_test.dart create mode 100644 test/dav/sync/dav_sync_engine_test.dart create mode 100644 test/dav/xml/dav_xml_test.dart create mode 100644 test/features/accounts/data/accounts_repository_test.dart create mode 100644 test/features/schedule/presentation/schedule_sidebar_provider_test.dart create mode 100644 test/fixtures/schema_v5_production_like.sql rename test/microsoft_todo/api/{microsoft_todo_google_tasks_adapter_test.dart => microsoft_todo_task_remote_client_test.dart} (66%) create mode 100644 test/providers/busy_provider_test.dart diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index d52c38a..3a0ddd9 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -1,14 +1,14 @@ { "@@locale": "ar", "appTitle": "BusyMax", - "connectGoogleAccount": "اربط حسابات Google وMicrosoft لمزامنة التقويمات والمهام.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "في شاشة أذونات Google، حدّد أذونات التقويم والمهام معًا.", "googlePermissionsRequiredRetry": "أذونات تقويم Google وGoogle Tasks مطلوبة. حاول مرة أخرى وحدّد مربعي الاختيار.", "finishSetup": "إنهاء الإعداد", "continueSetup": "متابعة", "onboardingSetupTitle": "إعداد BusyMax", "onboardingAccountsStepTitle": "ربط الحسابات", - "onboardingAccountsStepDescription": "أضف جميع حسابات Google وMicrosoft التي تريد استخدامها. يزامن BusyMax التقويمات والأحداث وقوائم المهام والمهام من كل حساب.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "اختيار إعدادات النظام", "onboardingPreferencesStepDescription": "اضبط سلوك التطبيق على سطح المكتب والتذكيرات ومستوى تفاصيل الإشعارات والمظهر قبل فتح جدولك.", "signInWithGoogle": "تسجيل الدخول باستخدام Google", @@ -40,7 +40,7 @@ "showInSchedule": "إظهار في الجدول", "noCalendarsSynced": "لم تتم مزامنة أي تقويمات بعد.", "allDay": "طوال اليوم", - "moreItems": "+\u2068{count}\u2069 عناصر أخرى", + "moreItems": "+⁨{count}⁩ عناصر أخرى", "noEventsOrTasks": "لا توجد أحداث أو مهام", "scheduleLoading": "جارٍ تحميل الجدول...", "scheduleUnavailable": "الجدول غير متاح", @@ -93,10 +93,10 @@ "formatItalicTooltip": "مائل", "formatUnderlineShortLabel": "U", "formatUnderlineTooltip": "تحته خط", - "reminderMinutesBefore": "{minutes, plural, =0{عند البدء} =1{قبل دقيقة واحدة} =2{قبل دقيقتين} few{قبل \u2068{minutes}\u2069 دقائق} many{قبل \u2068{minutes}\u2069 دقيقة} other{قبل \u2068{minutes}\u2069 دقيقة}}", + "reminderMinutesBefore": "{minutes, plural, =0{عند البدء} =1{قبل دقيقة واحدة} =2{قبل دقيقتين} few{قبل ⁨{minutes}⁩ دقائق} many{قبل ⁨{minutes}⁩ دقيقة} other{قبل ⁨{minutes}⁩ دقيقة}}", "reminderAtStart": "عند البدء", - "reminderHoursBefore": "{hours, plural, =0{عند البدء} =1{قبل ساعة واحدة} =2{قبل ساعتين} few{قبل \u2068{hours}\u2069 ساعات} many{قبل \u2068{hours}\u2069 ساعة} other{قبل \u2068{hours}\u2069 ساعة}}", - "reminderDaysBefore": "{days, plural, =0{في اليوم نفسه} =1{قبل يوم واحد} =2{قبل يومين} few{قبل \u2068{days}\u2069 أيام} many{قبل \u2068{days}\u2069 يومًا} other{قبل \u2068{days}\u2069 يوم}}", + "reminderHoursBefore": "{hours, plural, =0{عند البدء} =1{قبل ساعة واحدة} =2{قبل ساعتين} few{قبل ⁨{hours}⁩ ساعات} many{قبل ⁨{hours}⁩ ساعة} other{قبل ⁨{hours}⁩ ساعة}}", + "reminderDaysBefore": "{days, plural, =0{في اليوم نفسه} =1{قبل يوم واحد} =2{قبل يومين} few{قبل ⁨{days}⁩ أيام} many{قبل ⁨{days}⁩ يومًا} other{قبل ⁨{days}⁩ يوم}}", "availabilityFree": "متاح", "availabilityTentative": "مبدئي", "availabilityOutOfOffice": "خارج المكتب", @@ -109,7 +109,7 @@ "sensitivityPersonal": "شخصي", "tasks": "المهام", "allTasks": "كل المهام", - "tasksInList": "المهام في \u2068{title}\u2069", + "tasksInList": "المهام في ⁨{title}⁩", "taskLists": "قوائم المهام", "navigation": "التنقل", "mainMenu": "القائمة الرئيسية", @@ -164,7 +164,7 @@ "feedbackRateLimitedError": "أُرسلت ملاحظات كثيرة جدًا من هذه الشبكة. انتظر وحاول مرة أخرى.", "feedbackRejectedError": "رفض الخادم الإرسال. راجع الحقول وحاول مرة أخرى.", "feedbackServerError": "يتعذر على BusyStack قبول ملاحظاتك الآن. لم تُمسح ملاحظاتك؛ حاول مرة أخرى.", - "feedbackSuccess": "تم إرسال الملاحظات. المرجع: \u2068{id}\u2069", + "feedbackSuccess": "تم إرسال الملاحظات. المرجع: ⁨{id}⁩", "toggleSidebar": "إظهار الشريط الجانبي أو إخفاؤه", "showSidebar": "إظهار اللوحة الجانبية", "hideSidebar": "إخفاء اللوحة الجانبية", @@ -179,8 +179,8 @@ "removeAccount": "إزالة الحساب…", "removingAccount": "جارٍ إزالة الحساب…", "removeAccountDescription": "إيقاف المزامنة وإزالة بيانات هذا الحساب من هذا الجهاز.", - "removeAccountTitle": "إزالة \u2068{account}\u2069 من BusyMax؟", - "removeAccountConfirmation": "سيؤدي ذلك إلى حذف المهام والتقويمات والأحداث والتذكيرات والتغييرات غير المتصلة المعلّقة المخزّنة مؤقتًا من هذا الجهاز. ستُفقد التغييرات غير المتزامنة. لن يُحذف أي شيء من Google أو Microsoft.", + "removeAccountTitle": "إزالة ⁨{account}⁩ من BusyMax؟", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "إلغاء وصول BusyMax إلى حساب Google هذا أيضًا", "revokeGoogleAccessDescription": "ستحتاج إلى منح الوصول مرة أخرى قبل إعادة الاتصال.", "removeAccountAction": "إزالة الحساب", @@ -196,7 +196,7 @@ "deleteList": "حذف القائمة", "builtInMicrosoftList": "مدمجة", "builtInMicrosoftListCannotRenameDelete": "لا يمكن إعادة تسمية قوائم Microsoft To Do المدمجة أو حذفها.", - "deleteListConfirmation": "حذف «\u2068{title}\u2069» من Google Tasks؟", + "deleteListConfirmation": "حذف «⁨{title}⁩» من Google Tasks؟", "deleteEvent": "حذف الحدث", "title": "العنوان", "create": "إنشاء", @@ -206,9 +206,9 @@ "refreshAll": "تحديث الكل", "listRefreshed": "تم تحديث القائمة.", "allTasksRefreshed": "تم تحديث جميع الحسابات.", - "exportedFile": "تم التصدير إلى \u2068{path}\u2069", - "exportFailed": "فشل التصدير: \u2068{error}\u2069", - "refreshFailed": "فشل التحديث: \u2068{error}\u2069", + "exportedFile": "تم التصدير إلى ⁨{path}⁩", + "exportFailed": "فشل التصدير: ⁨{error}⁩", + "refreshFailed": "فشل التحديث: ⁨{error}⁩", "selectOrCreateTaskList": "اختر قائمة مهام أو أنشئ واحدة للبدء.", "signInToViewTasks": "سجّل الدخول لعرض المهام.", "noTasks": "لا توجد مهام.", @@ -221,8 +221,8 @@ "upcoming": "القادمة", "noDate": "بلا تاريخ", "completed": "مكتملة", - "duePrefix": "مستحقة في \u2068{date}\u2069", - "dateTimeDisplay": "\u2068{date}\u2069 · \u2068{time}\u2069", + "duePrefix": "مستحقة في ⁨{date}⁩", + "dateTimeDisplay": "⁨{date}⁩ · ⁨{time}⁩", "taskDetails": "تفاصيل المهمة", "editTask": "تعديل المهمة", "noTaskSelected": "لم يتم تحديد مهمة.", @@ -273,10 +273,11 @@ "list": "القائمة", "microsoftMoveUnsupported": "نقل المهام بين القوائم غير مدعوم لحسابات Microsoft To Do في هذا الإصدار.", "createSubtask": "إنشاء مهمة فرعية", + "subtasks": "مهام فرعية", "moveToTop": "نقل إلى الأعلى", "deleteTask": "حذف المهمة", "newSubtask": "مهمة فرعية جديدة", - "deleteTaskConfirmation": "حذف «\u2068{title}\u2069» من Google Tasks؟", + "deleteTaskConfirmation": "حذف «⁨{title}⁩»؟", "metadata": "البيانات الوصفية", "id": "المعرّف", "etag": "ETag", @@ -296,7 +297,7 @@ "startMinimizedToTray": "البدء مصغّرًا في شريط النظام", "requiresTrayIcon": "يتطلب أيقونة شريط النظام.", "syncComplete": "اكتملت المزامنة.", - "syncFailed": "فشلت المزامنة: \u2068{error}\u2069", + "syncFailed": "فشلت المزامنة: ⁨{error}⁩", "notifySyncFailures": "إشعارات عند فشل المزامنة", "notifyConflicts": "إشعارات عند حدوث تعارضات", "notifyDueToday": "إشعارات المهام المستحقة اليوم", @@ -325,7 +326,7 @@ "diagnostics": "التشخيصات", "apiInspectorDisabled": "إظهار فاحص API", "googleTasksApi": "واجهة Google Tasks API", - "discoveryRevision": "مراجعة Discovery: \u2068{revision}\u2069", + "discoveryRevision": "مراجعة Discovery: ⁨{revision}⁩", "implementedMethods": "الطرق المنفذة", "supportsTasksScopes": "يدعم نطاقَي tasks وtasks.readonly", "requiresTasksScope": "يتطلب نطاق tasks", @@ -333,9 +334,9 @@ "signInToInspectPendingOperations": "سجّل الدخول لفحص العمليات المعلّقة.", "noBlockedPendingOperations": "لا توجد عمليات معلّقة محظورة.", "operationActions": "إجراءات العملية", - "pendingOpListId": "القائمة=\u2068{id}\u2069", - "pendingOpTaskId": "المهمة=\u2068{id}\u2069", - "pendingOpAttempts": "المحاولات=\u2068{count}\u2069", + "pendingOpListId": "القائمة=⁨{id}⁩", + "pendingOpTaskId": "المهمة=⁨{id}⁩", + "pendingOpAttempts": "المحاولات=⁨{count}⁩", "retry": "إعادة المحاولة", "discard": "تجاهل", "discardChangesAction": "تجاهل التغييرات", @@ -346,11 +347,11 @@ "discardPendingOperationConfirmation": "سيؤدي ذلك إلى إزالة العملية المحلية المحظورة. ستُحدّث البيانات من Google Tasks في المزامنة التالية.", "pendingOperationDiscarded": "تم تجاهل العملية المعلّقة.", "syncFailureNotificationTitle": "فشلت مزامنة BusyMax", - "syncFailureNotificationBody": "فشلت المزامنة في الخلفية. \u2068{message}\u2069", + "syncFailureNotificationBody": "فشلت المزامنة في الخلفية. ⁨{message}⁩", "conflictNotificationTitle": "تعارض في مزامنة BusyMax", - "conflictNotificationBody": "تم حظر تغيير محلي معلّق. \u2068{summary}\u2069", + "conflictNotificationBody": "تم حظر تغيير محلي معلّق. ⁨{summary}⁩", "dueTodayNotificationTitle": "المهام المستحقة اليوم", - "dueTodayNotificationBody": "{count, plural, =0{لا توجد مهام مستحقة اليوم.} =1{هناك مهمة واحدة مستحقة اليوم.} =2{هناك مهمتان مستحقتان اليوم.} few{هناك \u2068{count}\u2069 مهام مستحقة اليوم.} many{هناك \u2068{count}\u2069 مهمة مستحقة اليوم.} other{هناك \u2068{count}\u2069 مهمة مستحقة اليوم.}}", + "dueTodayNotificationBody": "{count, plural, =0{لا توجد مهام مستحقة اليوم.} =1{هناك مهمة واحدة مستحقة اليوم.} =2{هناك مهمتان مستحقتان اليوم.} few{هناك ⁨{count}⁩ مهام مستحقة اليوم.} many{هناك ⁨{count}⁩ مهمة مستحقة اليوم.} other{هناك ⁨{count}⁩ مهمة مستحقة اليوم.}}", "eventReminderNotificationTitle": "تذكير بحدث", "taskReminderNotificationTitle": "تذكير بمهمة", "eventReminderNotificationBody": "سيبدأ الحدث قريبًا.", @@ -363,12 +364,208 @@ "previousYear": "السنة السابقة", "nextYear": "السنة التالية", "openYearView": "فتح عرض السنة", - "weekNumberTooltip": "الأسبوع \u2068{number}\u2069", + "weekNumberTooltip": "الأسبوع ⁨{number}⁩", "resizeAllDayPanel": "تغيير حجم لوحة اليوم الكامل", - "scheduleItemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{\u2068{count}\u2069 عناصر} many{\u2068{count}\u2069 عنصرًا} other{\u2068{count}\u2069 عنصر}}", + "scheduleItemCount": "{count, plural, =0{لا عناصر} =1{عنصر واحد} =2{عنصران} few{⁨{count}⁩ عناصر} many{⁨{count}⁩ عنصرًا} other{⁨{count}⁩ عنصر}}", "readOnlyCalendar": "هذا التقويم للقراءة فقط.", "selectTimeZone": "اختيار المنطقة الزمنية", "searchLocations": "البحث عن مواقع", "noLocationsFound": "لم يتم العثور على مواقع", - "deleteCalendarConfirmation": "حذف «\u2068{title}\u2069»؟" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "حذف «⁨{title}⁩»؟", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "يجب ألا يكون موعد الاستحقاق قبل وقت البدء.", + "taskStartDueTimeModeMismatch": "عيّن وقتًا لكل من البدء والاستحقاق، أو اجعل المهمة طوال اليوم.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "إلغاء المشاركة", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "تحتاج إلى إجراء", + "taskStatusInProcess": "تحت الإجراء", + "taskStatusCompleted": "مُكتمِل", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "الأولوية", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "لا توجد أي تذكيرات", + "editReminder": "Edit reminder", + "beforeTaskStarts": "قبل أن تبدأ المهمة", + "beforeTaskDue": "قبل اكتمال المهمة", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "تكرار كل", + "repeatOn": "Repeat on", + "repeatEnd": "نهاية التكرار", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "في يوم {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} مرات", + "repeatUntilSummary": "until {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "إخْفِ المهام الفرعية", + "hideClosedSubtasks": "إخف المهام الفرعية المغلقة", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f7814fc..fbf42e7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,14 +1,14 @@ { "@@locale": "de", "appTitle": "BusyMax", - "connectGoogleAccount": "Verbinden Sie Google- und Microsoft-Konten, um Kalender und Aufgaben zu synchronisieren.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Wählen Sie auf dem Google-Berechtigungsbildschirm sowohl Kalender- als auch Aufgabenberechtigungen aus.", "googlePermissionsRequiredRetry": "Die Berechtigungen für Google Kalender und Google Tasks sind erforderlich. Versuchen Sie es erneut und aktivieren Sie beide Kontrollkästchen.", "finishSetup": "Einrichtung abschließen", "continueSetup": "Weiter", "onboardingSetupTitle": "BusyMax einrichten", "onboardingAccountsStepTitle": "Konten verbinden", - "onboardingAccountsStepDescription": "Fügen Sie alle Google- und Microsoft-Konten hinzu, die Sie verwenden möchten. BusyMax synchronisiert Kalender, Termine, Aufgabenlisten und Aufgaben aus jedem Konto.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Systemeinstellungen wählen", "onboardingPreferencesStepDescription": "Legen Sie Desktop-Verhalten, Erinnerungen, Benachrichtigungsdetails und Darstellung fest, bevor Sie Ihren Zeitplan öffnen.", "signInWithGoogle": "Mit Google anmelden", @@ -94,12 +94,30 @@ "formatUnderlineShortLabel": "U", "formatUnderlineTooltip": "Unterstrichen", "reminderMinutesBefore": "{minutes, plural, =1{1 Minute vorher} other{{minutes} Minuten vorher}}", - "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}}, + "@reminderMinutesBefore": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "reminderAtStart": "Zum Startzeitpunkt", "reminderHoursBefore": "{hours, plural, =1{1 Stunde vorher} other{{hours} Stunden vorher}}", - "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}}, + "@reminderHoursBefore": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "reminderDaysBefore": "{days, plural, =1{1 Tag vorher} other{{days} Tage vorher}}", - "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}}, + "@reminderDaysBefore": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "availabilityFree": "Frei", "availabilityTentative": "Mit Vorbehalt", "availabilityOutOfOffice": "Abwesend", @@ -183,8 +201,14 @@ "removingAccount": "Konto wird entfernt…", "removeAccountDescription": "Synchronisierung beenden und die Daten dieses Kontos von diesem Gerät entfernen.", "removeAccountTitle": "{account} aus BusyMax entfernen?", - "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}}, - "removeAccountConfirmation": "Dadurch werden zwischengespeicherte Aufgaben, Kalender, Termine, Erinnerungen und ausstehende Offline-Änderungen von diesem Gerät gelöscht. Nicht synchronisierte Änderungen gehen verloren. Bei Google oder Microsoft wird nichts gelöscht.", + "@removeAccountTitle": { + "placeholders": { + "account": { + "type": "String" + } + } + }, + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "BusyMax-Zugriff auf dieses Google-Konto ebenfalls widerrufen", "revokeGoogleAccessDescription": "Vor einer erneuten Verbindung müssen Sie den Zugriff wieder gewähren.", "removeAccountAction": "Konto entfernen", @@ -211,9 +235,21 @@ "listRefreshed": "Liste aktualisiert.", "allTasksRefreshed": "Alle Konten aktualisiert.", "exportedFile": "Exportiert nach {path}", - "@exportedFile": {"placeholders": {"path": {"type": "String"}}}, + "@exportedFile": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "exportFailed": "Export fehlgeschlagen: {error}", - "@exportFailed": {"placeholders": {"error": {"type": "String"}}}, + "@exportFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "refreshFailed": "Aktualisierung fehlgeschlagen: {error}", "selectOrCreateTaskList": "Wählen oder erstellen Sie zunächst eine Aufgabenliste.", "signInToViewTasks": "Melden Sie sich an, um Aufgaben zu sehen.", @@ -279,10 +315,11 @@ "list": "Liste", "microsoftMoveUnsupported": "Das Verschieben zwischen Listen wird für Microsoft To Do-Konten in dieser Version nicht unterstützt.", "createSubtask": "Unteraufgabe erstellen", + "subtasks": "Unteraufgaben", "moveToTop": "Ganz nach oben verschieben", "deleteTask": "Aufgabe löschen", "newSubtask": "Neue Unteraufgabe", - "deleteTaskConfirmation": "\"{title}\" aus Google Tasks löschen?", + "deleteTaskConfirmation": "\"{title}\" löschen?", "metadata": "Metadaten", "id": "ID", "etag": "ETag", @@ -370,14 +407,228 @@ "nextYear": "Nächstes Jahr", "openYearView": "Jahresansicht öffnen", "weekNumberTooltip": "Woche {number}", - "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}}, + "@weekNumberTooltip": { + "placeholders": { + "number": { + "type": "int" + } + } + }, "resizeAllDayPanel": "Ganztägigen Bereich vergrößern oder verkleinern", "scheduleItemCount": "{count, plural, =1{1 Eintrag} other{{count} Einträge}}", - "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}}, + "@scheduleItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "readOnlyCalendar": "Dieser Kalender ist schreibgeschützt.", "selectTimeZone": "Zeitzone auswählen", "searchLocations": "Orte suchen", "noLocationsFound": "Keine Orte gefunden", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", "deleteCalendarConfirmation": "\"{title}\" löschen?", - "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}} + "@deleteCalendarConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Die Fälligkeit darf nicht vor dem Beginn liegen.", + "taskStartDueTimeModeMismatch": "Lege für Beginn und Fälligkeit jeweils eine Uhrzeit fest oder mache die Aufgabe ganztägig.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Freigabe aufheben", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Handlungsbedarf", + "taskStatusInProcess": "In Bearbeitung", + "taskStatusCompleted": "Fertiggestellt", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priorität", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "Keine Erinnerungen", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Bevor die Aufgabe startet", + "beforeTaskDue": "Bevor die Aufgabe fällig ist", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Erinnerungen verwerfen", + "keepRelatedReminders": "Erinnerungen behalten", + "repeatEvery": "Wiederhole jeden", + "repeatOn": "Repeat on", + "repeatEnd": "Wiederholung beenden", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "an {days}", + "repeatOnMonthDaysSummary": "am Tag {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} mal", + "repeatUntilSummary": "bis {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Aufgabe duplizieren", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Teilaufgaben ausblenden", + "hideClosedSubtasks": "Geschlossene Teilaufgaben ausblenden", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d462d4f..2d12274 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,14 +1,14 @@ { "@@locale": "en", "appTitle": "BusyMax", - "connectGoogleAccount": "Connect Google and Microsoft accounts to sync calendars and tasks.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "On the Google permission screen, select both Calendar and Tasks permissions.", "googlePermissionsRequiredRetry": "Google Calendar and Google Tasks permissions are required. Please try again and select both checkboxes.", "finishSetup": "Finish setup", "continueSetup": "Continue", "onboardingSetupTitle": "Set Up BusyMax", "onboardingAccountsStepTitle": "Connect accounts", - "onboardingAccountsStepDescription": "Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Choose system settings", "onboardingPreferencesStepDescription": "Set desktop behavior, reminders, notification detail, and appearance before opening your schedule.", "signInWithGoogle": "Sign in with Google", @@ -187,13 +187,19 @@ "removeAccountDescription": "Stop syncing and remove this account’s data from this device.", "removeAccountTitle": "Remove {account} from BusyMax?", "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}}, - "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Also revoke BusyMax’s access to this Google Account", "revokeGoogleAccessDescription": "You will need to grant access again before reconnecting.", "removeAccountAction": "Remove account", "removeAccountFailed": "Could not finish removing the account. Try again.", "accountRemovedGoogleRevokeFailed": "The account was removed from this device, but BusyMax could not revoke Google access. You can revoke it from your Google Account.", "newTaskList": "New task list", + "taskListCreateFailed": "Could not create the task list: {error}", + "@taskListCreateFailed": {"placeholders": {"error": {"type": "String"}}}, + "taskListRenameFailed": "Could not rename the task list: {error}", + "@taskListRenameFailed": {"placeholders": {"error": {"type": "String"}}}, + "taskListDeleteFailed": "Could not delete the task list: {error}", + "@taskListDeleteFailed": {"placeholders": {"error": {"type": "String"}}}, "signInToViewTaskLists": "Sign in to view task lists.", "noTaskListsSynced": "No task lists synced yet.", "listActions": "List actions", @@ -201,10 +207,17 @@ "delete": "Delete", "renameList": "Rename list", "deleteList": "Delete list", + "unshare": "Unshare", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", "builtInMicrosoftList": "Built-in", "builtInMicrosoftListCannotRenameDelete": "Built-in Microsoft To Do lists cannot be renamed or deleted.", "deleteListConfirmation": "Delete \"{title}\" from Google Tasks?", "@deleteListConfirmation": {"placeholders": {"title": {"type": "String"}}}, + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "@deleteTaskListConfirmation": {"placeholders": {"title": {"type": "String"}}}, + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "@unshareTaskListConfirmation": {"placeholders": {"title": {"type": "String"}}}, "deleteEvent": "Delete Event", "title": "Title", "create": "Create", @@ -252,6 +265,30 @@ "statusSection": "Status", "openStatus": "Open", "doneStatus": "Done", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "In process", + "taskStatusCompleted": "Completed", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "@completionPercent": {"placeholders": {"percent": {"type": "int"}}}, + "completionDate": "Completion date", + "priority": "Priority", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "@priorityHighValue": {"placeholders": {"priority": {"type": "int"}}}, + "priorityMediumValue": "Priority {priority} · Medium", + "@priorityMediumValue": {"placeholders": {"priority": {"type": "int"}}}, + "priorityLowValue": "Priority {priority} · Low", + "@priorityLowValue": {"placeholders": {"priority": {"type": "int"}}}, + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", "notes": "Notes", "dueDate": "Due date", "clearDueDate": "Clear due date", @@ -264,6 +301,32 @@ "reminderTime": "Reminder time", "reminder": "Reminder", "addReminder": "Add Reminder", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnit": "Unit", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "@relatedRemindersDescription": {"placeholders": {"count": {"type": "int"}}}, + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", "addGuest": "Add Guest", "addGuestEmail": "Add guest email", "removeReminder": "Remove reminder", @@ -275,6 +338,48 @@ "repeatWeekly": "Weekly", "repeatMonthly": "Monthly", "repeatYearly": "Yearly", + "repeatEvery": "Repeat every", + "repeatOn": "Repeat on", + "repeatEnd": "End repeat", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "@repeatEveryDays": {"placeholders": {"count": {"type": "int"}}}, + "repeatEveryWeeks": "Every {count} weeks", + "@repeatEveryWeeks": {"placeholders": {"count": {"type": "int"}}}, + "repeatEveryMonths": "Every {count} months", + "@repeatEveryMonths": {"placeholders": {"count": {"type": "int"}}}, + "repeatEveryYears": "Every {count} years", + "@repeatEveryYears": {"placeholders": {"count": {"type": "int"}}}, + "repeatOnDaysSummary": "on {days}", + "@repeatOnDaysSummary": {"placeholders": {"days": {"type": "String"}}}, + "repeatOnMonthDaysSummary": "on day {days}", + "@repeatOnMonthDaysSummary": {"placeholders": {"days": {"type": "String"}}}, + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "@repeatOnOrdinalSummary": {"placeholders": {"ordinal": {"type": "String"}, "days": {"type": "String"}}}, + "repeatInMonthsSummary": "in {months}", + "@repeatInMonthsSummary": {"placeholders": {"months": {"type": "String"}}}, + "repeatTimesSummary": "{count} times", + "@repeatTimesSummary": {"placeholders": {"count": {"type": "int"}}}, + "repeatUntilSummary": "until {date}", + "@repeatUntilSummary": {"placeholders": {"date": {"type": "String"}}}, + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", "importance": "Importance", "importanceLow": "Low", "importanceNormal": "Normal", @@ -291,10 +396,17 @@ "list": "List", "microsoftMoveUnsupported": "Moving between lists is not supported for Microsoft To Do accounts in this version.", "createSubtask": "Create subtask", + "subtasks": "Subtasks", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "@taskDuplicateFailed": {"placeholders": {"error": {"type": "String"}}}, + "hideSubtasks": "Hide subtasks", + "hideClosedSubtasks": "Hide closed subtasks", "moveToTop": "Move to top", "deleteTask": "Delete Task", "newSubtask": "New subtask", - "deleteTaskConfirmation": "Delete \"{title}\" from Google Tasks?", + "deleteTaskConfirmation": "Delete \"{title}\"?", "@deleteTaskConfirmation": {"placeholders": {"title": {"type": "String"}}}, "metadata": "Metadata", "id": "ID", @@ -402,6 +514,76 @@ "selectTimeZone": "Select Timezone", "searchLocations": "Search locations", "noLocationsFound": "No locations found", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": {"placeholders": {"time": {"type": "String"}}}, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": {"placeholders": {"host": {"type": "String"}}}, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": {"placeholders": {"time": {"type": "String"}}}, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": {"placeholders": {"code": {"type": "String"}}}, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": {"placeholders": {"time": {"type": "String"}}}, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": {"placeholders": {"summary": {"type": "String"}}}, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Due must not be before start.", + "taskStartDueTimeModeMismatch": "Set times for both start and due, or make the task all day.", "deleteCalendarConfirmation": "Delete \"{title}\"?", "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}} } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 32e81a3..1c24b87 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,14 +1,14 @@ { "@@locale": "es", "appTitle": "BusyMax", - "connectGoogleAccount": "Conecta cuentas de Google y Microsoft para sincronizar calendarios y tareas.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "En la pantalla de permisos de Google, selecciona los permisos de Calendario y Tareas.", "googlePermissionsRequiredRetry": "Los permisos de Google Calendar y Google Tasks son obligatorios. Inténtalo de nuevo y selecciona ambas casillas.", "finishSetup": "Finalizar configuración", "continueSetup": "Continuar", "onboardingSetupTitle": "Configurar BusyMax", "onboardingAccountsStepTitle": "Conectar cuentas", - "onboardingAccountsStepDescription": "Añade todas las cuentas de Google y Microsoft que quieras usar. BusyMax sincroniza calendarios, eventos, listas de tareas y tareas de cada cuenta.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Elegir ajustes del sistema", "onboardingPreferencesStepDescription": "Configura el comportamiento de la aplicación en el escritorio, los recordatorios, el nivel de detalle de las notificaciones y la apariencia antes de abrir tu agenda.", "signInWithGoogle": "Iniciar sesión con Google", @@ -94,12 +94,30 @@ "formatUnderlineShortLabel": "S", "formatUnderlineTooltip": "Subrayado", "reminderMinutesBefore": "{minutes, plural, =1{1 minuto antes} other{{minutes} minutos antes}}", - "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}}, + "@reminderMinutesBefore": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "reminderAtStart": "A la hora de inicio", "reminderHoursBefore": "{hours, plural, =1{1 hora antes} other{{hours} horas antes}}", - "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}}, + "@reminderHoursBefore": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "reminderDaysBefore": "{days, plural, =1{1 día antes} other{{days} días antes}}", - "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}}, + "@reminderDaysBefore": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "availabilityFree": "Libre", "availabilityTentative": "Provisional", "availabilityOutOfOffice": "Fuera de la oficina", @@ -183,8 +201,14 @@ "removingAccount": "Eliminando cuenta…", "removeAccountDescription": "Detener la sincronización y eliminar de este dispositivo los datos de esta cuenta.", "removeAccountTitle": "¿Eliminar {account} de BusyMax?", - "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}}, - "removeAccountConfirmation": "Esto elimina de este dispositivo las tareas, los calendarios, los eventos, los recordatorios y los cambios sin conexión pendientes almacenados en caché. Los cambios no sincronizados se perderán. No se eliminará nada de Google ni Microsoft.", + "@removeAccountTitle": { + "placeholders": { + "account": { + "type": "String" + } + } + }, + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Revocar también el acceso de BusyMax a esta cuenta de Google", "revokeGoogleAccessDescription": "Tendrás que volver a conceder acceso antes de reconectar la cuenta.", "removeAccountAction": "Eliminar cuenta", @@ -211,9 +235,21 @@ "listRefreshed": "Lista actualizada.", "allTasksRefreshed": "Todas las cuentas se actualizaron.", "exportedFile": "Exportado a {path}", - "@exportedFile": {"placeholders": {"path": {"type": "String"}}}, + "@exportedFile": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "exportFailed": "Error al exportar: {error}", - "@exportFailed": {"placeholders": {"error": {"type": "String"}}}, + "@exportFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "refreshFailed": "Error al actualizar: {error}", "selectOrCreateTaskList": "Selecciona o crea una lista de tareas para empezar.", "signInToViewTasks": "Inicia sesión para ver las tareas.", @@ -279,10 +315,11 @@ "list": "Lista", "microsoftMoveUnsupported": "En esta versión, no se pueden mover tareas entre listas en cuentas de Microsoft To Do.", "createSubtask": "Crear subtarea", + "subtasks": "Subtareas", "moveToTop": "Mover al principio", "deleteTask": "Eliminar tarea", "newSubtask": "Nueva subtarea", - "deleteTaskConfirmation": "¿Eliminar \"{title}\" de Google Tasks?", + "deleteTaskConfirmation": "¿Eliminar \"{title}\"?", "metadata": "Metadatos", "id": "ID", "etag": "ETag", @@ -370,14 +407,228 @@ "nextYear": "Año siguiente", "openYearView": "Abrir la vista anual", "weekNumberTooltip": "Semana {number}", - "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}}, + "@weekNumberTooltip": { + "placeholders": { + "number": { + "type": "int" + } + } + }, "resizeAllDayPanel": "Cambiar el tamaño del panel de todo el día", "scheduleItemCount": "{count, plural, =1{1 elemento} other{{count} elementos}}", - "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}}, + "@scheduleItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "readOnlyCalendar": "Este calendario es de solo lectura.", "selectTimeZone": "Seleccionar zona horaria", "searchLocations": "Buscar ubicaciones", "noLocationsFound": "No se encontraron ubicaciones", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", "deleteCalendarConfirmation": "¿Eliminar \"{title}\"?", - "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}} + "@deleteCalendarConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "El vencimiento no puede ser anterior al inicio.", + "taskStartDueTimeModeMismatch": "Define una hora tanto para el inicio como para el vencimiento, o configura la tarea para todo el día.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "No compartir", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Necesita una acción", + "taskStatusInProcess": "En proceso", + "taskStatusCompleted": "Completada", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Prioridad", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "Sin recordatorio", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Antes de empezar la tarea", + "beforeTaskDue": "Antes de terminar la tarea", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Descartar recordatorios", + "keepRelatedReminders": "Mantener recordatorios", + "repeatEvery": "Repetir cada", + "repeatOn": "Repeat on", + "repeatEnd": "Finalizar repetición", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "dentro de {days}", + "repeatOnMonthDaysSummary": "en día {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "en {months}", + "repeatTimesSummary": "{count} veces", + "repeatUntilSummary": "hasta el {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicar tarea", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Ocultar subtareas", + "hideClosedSubtasks": "Ocultar subtareas cerradas", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb index d45de47..ebd1e44 100644 --- a/lib/l10n/app_et.arb +++ b/lib/l10n/app_et.arb @@ -1,14 +1,14 @@ { "@@locale": "et", "appTitle": "BusyMax", - "connectGoogleAccount": "Ühendage Google'i ja Microsofti kontod kalendrite ja ülesannete sünkroonimiseks.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Valige Google'i õiguste kuval nii kalendri kui ka ülesannete õigused.", "googlePermissionsRequiredRetry": "Google Calendari ja Google Tasksi õigused on nõutavad. Proovige uuesti ja märkige mõlemad ruudud.", "finishSetup": "Lõpeta seadistamine", "continueSetup": "Jätka", "onboardingSetupTitle": "BusyMaxi seadistamine", "onboardingAccountsStepTitle": "Kontode ühendamine", - "onboardingAccountsStepDescription": "Lisage kõik Google'i ja Microsofti kontod, mida soovite kasutada. BusyMax sünkroonib iga konto kalendrid, sündmused, ülesandeloendid ja ülesanded.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Süsteemiseadete valimine", "onboardingPreferencesStepDescription": "Enne ajakava avamist määrake töölauakäitumine, meeldetuletused, teavituste üksikasjalikkus ja välimus.", "signInWithGoogle": "Logi Google'iga sisse", @@ -41,7 +41,13 @@ "noCalendarsSynced": "Ühtegi kalendrit pole veel sünkroonitud.", "allDay": "Kogu päev", "moreItems": "+{count} veel", - "@moreItems": {"placeholders": {"count": {"type": "int"}}}, + "@moreItems": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "noEventsOrTasks": "Sündmusi ega ülesandeid pole", "scheduleLoading": "Ajakava laadimine...", "scheduleUnavailable": "Ajakava pole saadaval", @@ -95,12 +101,30 @@ "formatUnderlineShortLabel": "A", "formatUnderlineTooltip": "Allajoonitud", "reminderMinutesBefore": "{minutes, plural, =1{1 minut varem} other{{minutes} minutit varem}}", - "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}}, + "@reminderMinutesBefore": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "reminderAtStart": "Algusajal", "reminderHoursBefore": "{hours, plural, =1{1 tund varem} other{{hours} tundi varem}}", - "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}}, + "@reminderHoursBefore": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "reminderDaysBefore": "{days, plural, =1{1 päev varem} other{{days} päeva varem}}", - "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}}, + "@reminderDaysBefore": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "availabilityFree": "Vaba", "availabilityTentative": "Esialgne", "availabilityOutOfOffice": "Kontorist väljas", @@ -114,7 +138,13 @@ "tasks": "Ülesanded", "allTasks": "Kõik ülesanded", "tasksInList": "Loendi „{title}” ülesanded", - "@tasksInList": {"placeholders": {"title": {"type": "String"}}}, + "@tasksInList": { + "placeholders": { + "title": { + "type": "String" + } + } + }, "taskLists": "Ülesandeloendid", "navigation": "Navigeerimine", "mainMenu": "Peamenüü", @@ -170,7 +200,13 @@ "feedbackRejectedError": "Server lükkas saatmise tagasi. Kontrollige välju ja proovige uuesti.", "feedbackServerError": "BusyStack ei saa praegu teie tagasisidet vastu võtta. Teie tagasisidet ei kustutatud; proovige uuesti.", "feedbackSuccess": "Tagasiside saadetud. Viide: {id}", - "@feedbackSuccess": {"placeholders": {"id": {"type": "String"}}}, + "@feedbackSuccess": { + "placeholders": { + "id": { + "type": "String" + } + } + }, "toggleSidebar": "Kuva või peida külgriba", "showSidebar": "Kuva külgpaneel", "hideSidebar": "Peida külgpaneel", @@ -186,8 +222,14 @@ "removingAccount": "Konto eemaldamine…", "removeAccountDescription": "Lõpeta sünkroonimine ja eemalda selle konto andmed seadmest.", "removeAccountTitle": "Kas eemaldada {account} BusyMaxist?", - "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}}, - "removeAccountConfirmation": "See kustutab seadmest vahemällu salvestatud ülesanded, kalendrid, sündmused, meeldetuletused ja sünkroonimist ootavad võrguühenduseta muudatused. Sünkroonimata muudatused lähevad kaotsi. Google'ist ega Microsoftist midagi ei kustutata.", + "@removeAccountTitle": { + "placeholders": { + "account": { + "type": "String" + } + } + }, + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Tühista ka BusyMaxi juurdepääs sellele Google'i kontole", "revokeGoogleAccessDescription": "Enne uuesti ühendamist peate juurdepääsu uuesti andma.", "removeAccountAction": "Eemalda konto", @@ -204,7 +246,13 @@ "builtInMicrosoftList": "Sisseehitatud", "builtInMicrosoftListCannotRenameDelete": "Microsoft To Do sisseehitatud loendeid ei saa ümber nimetada ega kustutada.", "deleteListConfirmation": "Kas kustutada „{title}” Google Tasksist?", - "@deleteListConfirmation": {"placeholders": {"title": {"type": "String"}}}, + "@deleteListConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, "deleteEvent": "Kustuta sündmus", "title": "Pealkiri", "create": "Loo", @@ -215,11 +263,29 @@ "listRefreshed": "Loend on värskendatud.", "allTasksRefreshed": "Kõik kontod on värskendatud.", "exportedFile": "Eksporditud asukohta {path}", - "@exportedFile": {"placeholders": {"path": {"type": "String"}}}, + "@exportedFile": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "exportFailed": "Eksportimine nurjus: {error}", - "@exportFailed": {"placeholders": {"error": {"type": "String"}}}, + "@exportFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "refreshFailed": "Värskendamine nurjus: {error}", - "@refreshFailed": {"placeholders": {"error": {"type": "String"}}}, + "@refreshFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "selectOrCreateTaskList": "Alustamiseks valige või looge ülesandeloend.", "signInToViewTasks": "Ülesannete vaatamiseks logige sisse.", "noTasks": "Ülesandeid pole.", @@ -233,12 +299,22 @@ "noDate": "Kuupäevata", "completed": "Lõpetatud", "duePrefix": "Tähtaeg {date}", - "@duePrefix": {"placeholders": {"date": {"type": "String"}}}, + "@duePrefix": { + "placeholders": { + "date": { + "type": "String" + } + } + }, "dateTimeDisplay": "{date} · {time}", "@dateTimeDisplay": { "placeholders": { - "date": {"type": "String"}, - "time": {"type": "String"} + "date": { + "type": "String" + }, + "time": { + "type": "String" + } } }, "taskDetails": "Ülesande üksikasjad", @@ -291,11 +367,18 @@ "list": "Loend", "microsoftMoveUnsupported": "Selles versioonis ei toetata Microsoft To Do kontodel ülesannete teisaldamist loendite vahel.", "createSubtask": "Loo alamülesanne", + "subtasks": "Alamülesanded", "moveToTop": "Teisalda kõige üles", "deleteTask": "Kustuta ülesanne", "newSubtask": "Uus alamülesanne", - "deleteTaskConfirmation": "Kas kustutada „{title}” Google Tasksist?", - "@deleteTaskConfirmation": {"placeholders": {"title": {"type": "String"}}}, + "deleteTaskConfirmation": "Kas kustutada „{title}”?", + "@deleteTaskConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, "metadata": "Metaandmed", "id": "ID", "etag": "ETag", @@ -316,7 +399,13 @@ "requiresTrayIcon": "Nõuab süsteemisalve ikooni.", "syncComplete": "Sünkroonimine on lõpetatud.", "syncFailed": "Sünkroonimine nurjus: {error}", - "@syncFailed": {"placeholders": {"error": {"type": "String"}}}, + "@syncFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "notifySyncFailures": "Teavitused sünkroonimise nurjumisel", "notifyConflicts": "Teavitused konfliktide korral", "notifyDueToday": "Täna tähtuvate ülesannete teavitused", @@ -346,7 +435,13 @@ "apiInspectorDisabled": "Kuva API-inspektor", "googleTasksApi": "Google Tasks API", "discoveryRevision": "Discovery versioon: {revision}", - "@discoveryRevision": {"placeholders": {"revision": {"type": "String"}}}, + "@discoveryRevision": { + "placeholders": { + "revision": { + "type": "String" + } + } + }, "implementedMethods": "Rakendatud meetodid", "supportsTasksScopes": "Toetab õiguse ulatusi tasks ja tasks.readonly", "requiresTasksScope": "Nõuab õiguse ulatust tasks", @@ -355,11 +450,29 @@ "noBlockedPendingOperations": "Blokeeritud ootel toiminguid pole.", "operationActions": "Toimingu tegevused", "pendingOpListId": "loend={id}", - "@pendingOpListId": {"placeholders": {"id": {"type": "String"}}}, + "@pendingOpListId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, "pendingOpTaskId": "ülesanne={id}", - "@pendingOpTaskId": {"placeholders": {"id": {"type": "String"}}}, + "@pendingOpTaskId": { + "placeholders": { + "id": { + "type": "String" + } + } + }, "pendingOpAttempts": "katseid={count}", - "@pendingOpAttempts": {"placeholders": {"count": {"type": "int"}}}, + "@pendingOpAttempts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "retry": "Proovi uuesti", "discard": "Hülga", "discardChangesAction": "Hülga", @@ -371,13 +484,31 @@ "pendingOperationDiscarded": "Ootel toiming hüljatud.", "syncFailureNotificationTitle": "BusyMaxi sünkroonimine nurjus", "syncFailureNotificationBody": "Taustal sünkroonimine nurjus. {message}", - "@syncFailureNotificationBody": {"placeholders": {"message": {"type": "String"}}}, + "@syncFailureNotificationBody": { + "placeholders": { + "message": { + "type": "String" + } + } + }, "conflictNotificationTitle": "BusyMaxi sünkroonimiskonflikt", "conflictNotificationBody": "Ootel kohalik muudatus blokeeriti. {summary}", - "@conflictNotificationBody": {"placeholders": {"summary": {"type": "String"}}}, + "@conflictNotificationBody": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, "dueTodayNotificationTitle": "Täna tähtuvad ülesanded", "dueTodayNotificationBody": "{count, plural, =1{Üks ülesanne tähtub täna.} other{{count} ülesannet tähtub täna.}}", - "@dueTodayNotificationBody": {"placeholders": {"count": {"type": "int"}}}, + "@dueTodayNotificationBody": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "eventReminderNotificationTitle": "Sündmuse meeldetuletus", "taskReminderNotificationTitle": "Ülesande meeldetuletus", "eventReminderNotificationBody": "Sündmus algab varsti.", @@ -391,14 +522,228 @@ "nextYear": "Järgmine aasta", "openYearView": "Ava aastavaade", "weekNumberTooltip": "Nädal {number}", - "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}}, + "@weekNumberTooltip": { + "placeholders": { + "number": { + "type": "int" + } + } + }, "resizeAllDayPanel": "Muuda kogu päeva paneeli suurust", "scheduleItemCount": "{count, plural, =1{1 kirje} other{{count} kirjet}}", - "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}}, + "@scheduleItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "readOnlyCalendar": "See kalender on kirjutuskaitstud.", "selectTimeZone": "Valige ajavöönd", "searchLocations": "Otsi asukohti", "noLocationsFound": "Asukohti ei leitud", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", "deleteCalendarConfirmation": "Kas kustutada „{title}”?", - "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}} + "@deleteCalendarConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Tähtaeg ei tohi olla enne algust.", + "taskStartDueTimeModeMismatch": "Määra nii algus- kui ka tähtaja kellaaeg või tee ülesanne kogu päeva ülesandeks.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Unshare", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "In process", + "taskStatusCompleted": "Completed", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priority", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Repeat every", + "repeatOn": "Repeat on", + "repeatEnd": "End repeat", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} times", + "repeatUntilSummary": "until {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Hide subtasks", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index ff614f5..d98cd8b 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -1,14 +1,14 @@ { "@@locale": "fa", "appTitle": "BusyMax", - "connectGoogleAccount": "حساب‌های Google و Microsoft را متصل کنید تا تقویم‌ها و کارها همگام شوند.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "در صفحهٔ مجوزهای Google، مجوزهای تقویم و کارها را هر دو انتخاب کنید.", "googlePermissionsRequiredRetry": "مجوزهای Google Calendar و Google Tasks لازم هستند. دوباره تلاش کنید و هر دو کادر را علامت بزنید.", "finishSetup": "پایان راه‌اندازی", "continueSetup": "ادامه", "onboardingSetupTitle": "راه‌اندازی BusyMax", "onboardingAccountsStepTitle": "اتصال حساب‌ها", - "onboardingAccountsStepDescription": "همهٔ حساب‌های Google و Microsoft موردنظرتان را اضافه کنید. BusyMax تقویم‌ها، رویدادها، فهرست‌های کار و کارهای هر حساب را همگام می‌کند.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "انتخاب تنظیمات سیستم", "onboardingPreferencesStepDescription": "پیش از باز کردن برنامه، رفتار برنامه روی میزکار، یادآورها، سطح جزئیات اعلان‌ها و ظاهر را تنظیم کنید.", "signInWithGoogle": "ورود با Google", @@ -40,7 +40,7 @@ "showInSchedule": "نمایش در برنامه", "noCalendarsSynced": "هنوز هیچ تقویمی همگام نشده است.", "allDay": "تمام روز", - "moreItems": "+\u2068{count}\u2069 مورد دیگر", + "moreItems": "+⁨{count}⁩ مورد دیگر", "noEventsOrTasks": "هیچ رویداد یا کاری وجود ندارد", "scheduleLoading": "در حال بارگیری برنامه...", "scheduleUnavailable": "برنامه در دسترس نیست", @@ -93,10 +93,10 @@ "formatItalicTooltip": "مورب", "formatUnderlineShortLabel": "U", "formatUnderlineTooltip": "زیرخط", - "reminderMinutesBefore": "{minutes, plural, =0{هنگام شروع} =1{یک دقیقه قبل} other{\u2068{minutes}\u2069 دقیقه قبل}}", + "reminderMinutesBefore": "{minutes, plural, =0{هنگام شروع} =1{یک دقیقه قبل} other{⁨{minutes}⁩ دقیقه قبل}}", "reminderAtStart": "هنگام شروع", - "reminderHoursBefore": "{hours, plural, =0{هنگام شروع} =1{یک ساعت قبل} other{\u2068{hours}\u2069 ساعت قبل}}", - "reminderDaysBefore": "{days, plural, =0{همان روز} =1{یک روز قبل} other{\u2068{days}\u2069 روز قبل}}", + "reminderHoursBefore": "{hours, plural, =0{هنگام شروع} =1{یک ساعت قبل} other{⁨{hours}⁩ ساعت قبل}}", + "reminderDaysBefore": "{days, plural, =0{همان روز} =1{یک روز قبل} other{⁨{days}⁩ روز قبل}}", "availabilityFree": "آزاد", "availabilityTentative": "احتمالی", "availabilityOutOfOffice": "خارج از دفتر", @@ -109,7 +109,7 @@ "sensitivityPersonal": "شخصی", "tasks": "کارها", "allTasks": "همهٔ کارها", - "tasksInList": "کارهای \u2068{title}\u2069", + "tasksInList": "کارهای ⁨{title}⁩", "taskLists": "فهرست‌های کار", "navigation": "پیمایش", "mainMenu": "منوی اصلی", @@ -164,7 +164,7 @@ "feedbackRateLimitedError": "بازخوردهای بیش از حدی از این شبکه ارسال شده است. کمی صبر کنید و دوباره تلاش کنید.", "feedbackRejectedError": "سرور ارسال را رد کرد. فیلدها را بررسی و دوباره تلاش کنید.", "feedbackServerError": "BusyStack اکنون نمی‌تواند بازخورد شما را بپذیرد. بازخورد شما پاک نشده است؛ دوباره تلاش کنید.", - "feedbackSuccess": "بازخورد ارسال شد. شناسهٔ پیگیری: \u2068{id}\u2069", + "feedbackSuccess": "بازخورد ارسال شد. شناسهٔ پیگیری: ⁨{id}⁩", "toggleSidebar": "نمایش یا پنهان کردن نوار کناری", "showSidebar": "نمایش پنل کناری", "hideSidebar": "پنهان کردن پنل کناری", @@ -179,8 +179,8 @@ "removeAccount": "حذف حساب…", "removingAccount": "در حال حذف حساب…", "removeAccountDescription": "همگام‌سازی را متوقف و داده‌های این حساب را از این دستگاه حذف کنید.", - "removeAccountTitle": "حذف \u2068{account}\u2069 از BusyMax؟", - "removeAccountConfirmation": "با این کار، کارها، تقویم‌ها، رویدادها، یادآورها و تغییرات آفلاین در انتظار از حافظهٔ نهان این دستگاه حذف می‌شوند. تغییرات همگام‌نشده از دست می‌روند. هیچ چیزی از Google یا Microsoft حذف نمی‌شود.", + "removeAccountTitle": "حذف ⁨{account}⁩ از BusyMax؟", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "دسترسی BusyMax به این حساب Google نیز لغو شود", "revokeGoogleAccessDescription": "پیش از اتصال دوباره باید دسترسی را دوباره اعطا کنید.", "removeAccountAction": "حذف حساب", @@ -196,7 +196,7 @@ "deleteList": "حذف فهرست", "builtInMicrosoftList": "داخلی", "builtInMicrosoftListCannotRenameDelete": "فهرست‌های داخلی Microsoft To Do را نمی‌توان تغییر نام داد یا حذف کرد.", - "deleteListConfirmation": "«\u2068{title}\u2069» از Google Tasks حذف شود؟", + "deleteListConfirmation": "«⁨{title}⁩» از Google Tasks حذف شود؟", "deleteEvent": "حذف رویداد", "title": "عنوان", "create": "ایجاد", @@ -206,9 +206,9 @@ "refreshAll": "تازه‌سازی همه", "listRefreshed": "فهرست تازه‌سازی شد.", "allTasksRefreshed": "همهٔ حساب‌ها تازه‌سازی شدند.", - "exportedFile": "در \u2068{path}\u2069 خروجی گرفته شد", - "exportFailed": "خروجی گرفتن ناموفق بود: \u2068{error}\u2069", - "refreshFailed": "تازه‌سازی ناموفق بود: \u2068{error}\u2069", + "exportedFile": "در ⁨{path}⁩ خروجی گرفته شد", + "exportFailed": "خروجی گرفتن ناموفق بود: ⁨{error}⁩", + "refreshFailed": "تازه‌سازی ناموفق بود: ⁨{error}⁩", "selectOrCreateTaskList": "برای شروع، یک فهرست کار انتخاب یا ایجاد کنید.", "signInToViewTasks": "برای دیدن کارها وارد شوید.", "noTasks": "هیچ کاری وجود ندارد.", @@ -221,8 +221,8 @@ "upcoming": "پیش رو", "noDate": "بدون تاریخ", "completed": "انجام‌شده", - "duePrefix": "سررسید: \u2068{date}\u2069", - "dateTimeDisplay": "\u2068{date}\u2069 · \u2068{time}\u2069", + "duePrefix": "سررسید: ⁨{date}⁩", + "dateTimeDisplay": "⁨{date}⁩ · ⁨{time}⁩", "taskDetails": "جزئیات کار", "editTask": "ویرایش کار", "noTaskSelected": "هیچ کاری انتخاب نشده است.", @@ -273,10 +273,11 @@ "list": "فهرست", "microsoftMoveUnsupported": "در این نسخه، جابه‌جایی کارها بین فهرست‌های حساب Microsoft To Do پشتیبانی نمی‌شود.", "createSubtask": "ایجاد زیرکار", + "subtasks": "زیرکارها", "moveToTop": "انتقال به بالاترین جایگاه", "deleteTask": "حذف کار", "newSubtask": "زیرکار جدید", - "deleteTaskConfirmation": "«\u2068{title}\u2069» از Google Tasks حذف شود؟", + "deleteTaskConfirmation": "«⁨{title}⁩» حذف شود؟", "metadata": "فراداده", "id": "شناسه", "etag": "ETag", @@ -296,7 +297,7 @@ "startMinimizedToTray": "شروع به‌صورت کوچک‌شده در سینی سیستم", "requiresTrayIcon": "به نماد سینی سیستم نیاز دارد.", "syncComplete": "همگام‌سازی کامل شد.", - "syncFailed": "همگام‌سازی ناموفق بود: \u2068{error}\u2069", + "syncFailed": "همگام‌سازی ناموفق بود: ⁨{error}⁩", "notifySyncFailures": "اعلان هنگام شکست همگام‌سازی", "notifyConflicts": "اعلان هنگام تداخل", "notifyDueToday": "اعلان کارهای دارای سررسید امروز", @@ -325,7 +326,7 @@ "diagnostics": "اطلاعات تشخیصی", "apiInspectorDisabled": "نمایش بازرس API", "googleTasksApi": "رابط Google Tasks API", - "discoveryRevision": "بازبینی Discovery: \u2068{revision}\u2069", + "discoveryRevision": "بازبینی Discovery: ⁨{revision}⁩", "implementedMethods": "روش‌های پیاده‌سازی‌شده", "supportsTasksScopes": "از محدوده‌های tasks و tasks.readonly پشتیبانی می‌کند", "requiresTasksScope": "به محدودهٔ tasks نیاز دارد", @@ -333,9 +334,9 @@ "signInToInspectPendingOperations": "برای بررسی عملیات در انتظار وارد شوید.", "noBlockedPendingOperations": "هیچ عملیات در انتظار مسدودشده‌ای وجود ندارد.", "operationActions": "اقدامات عملیات", - "pendingOpListId": "فهرست=\u2068{id}\u2069", - "pendingOpTaskId": "کار=\u2068{id}\u2069", - "pendingOpAttempts": "تلاش‌ها=\u2068{count}\u2069", + "pendingOpListId": "فهرست=⁨{id}⁩", + "pendingOpTaskId": "کار=⁨{id}⁩", + "pendingOpAttempts": "تلاش‌ها=⁨{count}⁩", "retry": "تلاش دوباره", "discard": "کنار گذاشتن", "discardChangesAction": "ذخیره نشود", @@ -346,11 +347,11 @@ "discardPendingOperationConfirmation": "با این کار عملیات محلی مسدودشده حذف می‌شود. در همگام‌سازی بعدی، داده‌ها از Google Tasks تازه‌سازی می‌شوند.", "pendingOperationDiscarded": "عملیات در انتظار کنار گذاشته شد.", "syncFailureNotificationTitle": "همگام‌سازی BusyMax ناموفق بود", - "syncFailureNotificationBody": "همگام‌سازی پس‌زمینه ناموفق بود. \u2068{message}\u2069", + "syncFailureNotificationBody": "همگام‌سازی پس‌زمینه ناموفق بود. ⁨{message}⁩", "conflictNotificationTitle": "تداخل همگام‌سازی BusyMax", - "conflictNotificationBody": "یک تغییر محلی در انتظار مسدود شد. \u2068{summary}\u2069", + "conflictNotificationBody": "یک تغییر محلی در انتظار مسدود شد. ⁨{summary}⁩", "dueTodayNotificationTitle": "کارهای دارای سررسید امروز", - "dueTodayNotificationBody": "{count, plural, =0{امروز هیچ کاری سررسید ندارد.} =1{امروز یک کار سررسید دارد.} other{امروز \u2068{count}\u2069 کار سررسید دارند.}}", + "dueTodayNotificationBody": "{count, plural, =0{امروز هیچ کاری سررسید ندارد.} =1{امروز یک کار سررسید دارد.} other{امروز ⁨{count}⁩ کار سررسید دارند.}}", "eventReminderNotificationTitle": "یادآور رویداد", "taskReminderNotificationTitle": "یادآور کار", "eventReminderNotificationBody": "رویداد به‌زودی شروع می‌شود.", @@ -363,14 +364,52 @@ "previousYear": "سال قبل", "nextYear": "سال بعد", "openYearView": "باز کردن نمای سال", - "weekNumberTooltip": "هفتهٔ \u2068{number}\u2069", + "weekNumberTooltip": "هفتهٔ ⁨{number}⁩", "resizeAllDayPanel": "تغییر اندازهٔ پنل تمام‌روز", - "scheduleItemCount": "{count, plural, =0{هیچ موردی} =1{یک مورد} other{\u2068{count}\u2069 مورد}}", + "scheduleItemCount": "{count, plural, =0{هیچ موردی} =1{یک مورد} other{⁨{count}⁩ مورد}}", "readOnlyCalendar": "این تقویم فقط‌خواندنی است.", "selectTimeZone": "انتخاب منطقهٔ زمانی", "searchLocations": "جست‌وجوی مکان‌ها", "noLocationsFound": "مکانی پیدا نشد", - "deleteCalendarConfirmation": "«\u2068{title}\u2069» حذف شود؟", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "«⁨{title}⁩» حذف شود؟", "@moreItems": { "placeholders": { "count": { @@ -434,5 +473,163 @@ "format": "decimalPattern" } } - } + }, + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "زمان سررسید نباید پیش از زمان شروع باشد.", + "taskStartDueTimeModeMismatch": "برای شروع و سررسید هر دو زمان تعیین کنید، یا کار را تمام‌روز کنید.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "لغو اشتراک‌گذاری", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "نیاز به اقدام", + "taskStatusInProcess": "در حال انجام", + "taskStatusCompleted": "انجام‌شده", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "اولویت", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "بدون یادآوری", + "editReminder": "Edit reminder", + "beforeTaskStarts": "قبل از شروع کار", + "beforeTaskDue": "قبل از موعد انجام کار", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "حذف یادآوری‌ها", + "keepRelatedReminders": "نگه‌داشتن یادآوری‌ها", + "repeatEvery": "تکرار هر", + "repeatOn": "Repeat on", + "repeatEnd": "پایان تکرار", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "در {days}", + "repeatOnMonthDaysSummary": "در روز {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "در {months}", + "repeatTimesSummary": "{count} بار", + "repeatUntilSummary": "تا {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "تکراری‌سازی وظیفه", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "مخفی‌سازی زیروظیفه‌ها", + "hideClosedSubtasks": "مخفی‌سازی زیروظیفه‌های بسته‌شده", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 6ea730d..c4546a6 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -1,14 +1,14 @@ { "@@locale": "fi", "appTitle": "BusyMax", - "connectGoogleAccount": "Yhdistä Google- ja Microsoft-tilit kalenterien ja tehtävien synkronointia varten.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Valitse Googlen käyttöoikeusnäkymässä sekä Kalenteri- että Tehtävät-käyttöoikeudet.", "googlePermissionsRequiredRetry": "Google Kalenterin ja Google Tasksin käyttöoikeudet vaaditaan. Yritä uudelleen ja valitse molemmat valintaruudut.", "finishSetup": "Viimeistele määritys", "continueSetup": "Jatka", "onboardingSetupTitle": "Määritä BusyMax", "onboardingAccountsStepTitle": "Yhdistä tilit", - "onboardingAccountsStepDescription": "Lisää kaikki haluamasi Google- ja Microsoft-tilit. BusyMax synkronoi kunkin tilin kalenterit, tapahtumat, tehtäväluettelot ja tehtävät.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Valitse järjestelmäasetukset", "onboardingPreferencesStepDescription": "Määritä sovelluksen toiminta työpöydällä, muistutukset, ilmoitusten yksityiskohtaisuus ja ulkoasu ennen aikataulun avaamista.", "signInWithGoogle": "Kirjaudu Google-tilillä", @@ -180,7 +180,7 @@ "removingAccount": "Poistetaan tiliä…", "removeAccountDescription": "Lopeta synkronointi ja poista tämän tilin tiedot tältä laitteelta.", "removeAccountTitle": "Poistetaanko {account} BusyMaxista?", - "removeAccountConfirmation": "Tämä poistaa välimuistissa olevat tehtävät, kalenterit, tapahtumat, muistutukset ja odottavat offline-muutokset tältä laitteelta. Synkronoimattomat muutokset menetetään. Mitään ei poisteta Googlesta tai Microsoftista.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Peruuta myös BusyMaxin käyttöoikeus tähän Google-tiliin", "revokeGoogleAccessDescription": "Käyttöoikeus on myönnettävä uudelleen ennen tilin yhdistämistä.", "removeAccountAction": "Poista tili", @@ -273,10 +273,11 @@ "list": "Luettelo", "microsoftMoveUnsupported": "Luettelosta toiseen siirtämistä ei tueta Microsoft To Do -tileillä tässä versiossa.", "createSubtask": "Luo alitehtävä", + "subtasks": "Alitehtävät", "moveToTop": "Siirrä ylimmäksi", "deleteTask": "Poista tehtävä", "newSubtask": "Uusi alitehtävä", - "deleteTaskConfirmation": "Poistetaanko \"{title}\" Google Tasksista?", + "deleteTaskConfirmation": "Poistetaanko \"{title}\"?", "metadata": "Metatiedot", "id": "Tunnus", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "Valitse aikavyöhyke", "searchLocations": "Hae sijainteja", "noLocationsFound": "Sijainteja ei löytynyt", - "deleteCalendarConfirmation": "Poistetaanko \"{title}\"?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "Poistetaanko \"{title}\"?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Määräaika ei saa olla ennen alkamisaikaa.", + "taskStartDueTimeModeMismatch": "Aseta kellonaika sekä alkamiselle että määräajalle tai tee tehtävästä koko päivän tehtävä.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Lopeta jakaminen", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Vaatii toimenpiteitä", + "taskStatusInProcess": "Käsittelyssä", + "taskStatusCompleted": "Valmiina", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Tärkeys", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Toista joka", + "repeatOn": "Repeat on", + "repeatEnd": "Lopeta toisto", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "päivänä {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} kertaa", + "repeatUntilSummary": "{date} asti", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Piilota alitehtävät", + "hideClosedSubtasks": "Piilota suljetut alitehtävät", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d0d61a0..8ffb024 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,14 +1,14 @@ { "@@locale": "fr", "appTitle": "BusyMax", - "connectGoogleAccount": "Connectez des comptes Google et Microsoft pour synchroniser calendriers et tâches.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Sur l’écran d’autorisations Google, sélectionnez les autorisations Calendrier et Tâches.", "googlePermissionsRequiredRetry": "Les autorisations Google Calendar et Google Tasks sont requises. Réessayez et sélectionnez les deux cases.", "finishSetup": "Terminer la configuration", "continueSetup": "Continuer", "onboardingSetupTitle": "Configurer BusyMax", "onboardingAccountsStepTitle": "Connecter des comptes", - "onboardingAccountsStepDescription": "Ajoutez tous les comptes Google et Microsoft que vous voulez utiliser. BusyMax synchronise les calendriers, événements, listes de tâches et tâches de chaque compte.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Choisir les paramètres système", "onboardingPreferencesStepDescription": "Réglez le comportement de l’application sur le bureau, les rappels, le niveau de détail des notifications et l’apparence avant d’ouvrir votre planning.", "signInWithGoogle": "Se connecter avec Google", @@ -94,12 +94,30 @@ "formatUnderlineShortLabel": "S", "formatUnderlineTooltip": "Souligné", "reminderMinutesBefore": "{minutes, plural, =1{1 minute avant} other{{minutes} minutes avant}}", - "@reminderMinutesBefore": {"placeholders": {"minutes": {"type": "int"}}}, + "@reminderMinutesBefore": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, "reminderAtStart": "À l’heure de début", "reminderHoursBefore": "{hours, plural, =1{1 heure avant} other{{hours} heures avant}}", - "@reminderHoursBefore": {"placeholders": {"hours": {"type": "int"}}}, + "@reminderHoursBefore": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, "reminderDaysBefore": "{days, plural, =1{1 jour avant} other{{days} jours avant}}", - "@reminderDaysBefore": {"placeholders": {"days": {"type": "int"}}}, + "@reminderDaysBefore": { + "placeholders": { + "days": { + "type": "int" + } + } + }, "availabilityFree": "Disponible", "availabilityTentative": "Provisoire", "availabilityOutOfOffice": "Absent du bureau", @@ -183,8 +201,14 @@ "removingAccount": "Suppression du compte…", "removeAccountDescription": "Arrêter la synchronisation et supprimer les données de ce compte de cet appareil.", "removeAccountTitle": "Supprimer {account} de BusyMax ?", - "@removeAccountTitle": {"placeholders": {"account": {"type": "String"}}}, - "removeAccountConfirmation": "Cette action supprime de cet appareil les tâches, calendriers, événements, rappels et modifications hors ligne en attente mis en cache. Les modifications non synchronisées seront perdues. Aucune donnée ne sera supprimée de Google ou Microsoft.", + "@removeAccountTitle": { + "placeholders": { + "account": { + "type": "String" + } + } + }, + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Révoquer également l’accès de BusyMax à ce compte Google", "revokeGoogleAccessDescription": "Vous devrez accorder à nouveau l’accès avant de reconnecter le compte.", "removeAccountAction": "Supprimer le compte", @@ -211,9 +235,21 @@ "listRefreshed": "Liste actualisée.", "allTasksRefreshed": "Tous les comptes ont été actualisés.", "exportedFile": "Exporté vers {path}", - "@exportedFile": {"placeholders": {"path": {"type": "String"}}}, + "@exportedFile": { + "placeholders": { + "path": { + "type": "String" + } + } + }, "exportFailed": "Échec de l’export : {error}", - "@exportFailed": {"placeholders": {"error": {"type": "String"}}}, + "@exportFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "refreshFailed": "Échec de l’actualisation : {error}", "selectOrCreateTaskList": "Sélectionnez ou créez une liste de tâches pour commencer.", "signInToViewTasks": "Connectez-vous pour voir les tâches.", @@ -279,10 +315,11 @@ "list": "Liste", "microsoftMoveUnsupported": "Le déplacement entre listes n’est pas pris en charge pour les comptes Microsoft To Do dans cette version.", "createSubtask": "Créer une sous-tâche", + "subtasks": "Sous-tâches", "moveToTop": "Déplacer tout en haut", "deleteTask": "Supprimer la tâche", "newSubtask": "Nouvelle sous-tâche", - "deleteTaskConfirmation": "Supprimer « {title} » de Google Tasks ?", + "deleteTaskConfirmation": "Supprimer « {title} » ?", "metadata": "Métadonnées", "id": "ID", "etag": "ETag", @@ -370,14 +407,228 @@ "nextYear": "Année suivante", "openYearView": "Ouvrir la vue annuelle", "weekNumberTooltip": "Semaine {number}", - "@weekNumberTooltip": {"placeholders": {"number": {"type": "int"}}}, + "@weekNumberTooltip": { + "placeholders": { + "number": { + "type": "int" + } + } + }, "resizeAllDayPanel": "Redimensionner le volet des événements sur toute la journée", "scheduleItemCount": "{count, plural, =1{1 élément} other{{count} éléments}}", - "@scheduleItemCount": {"placeholders": {"count": {"type": "int"}}}, + "@scheduleItemCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "readOnlyCalendar": "Ce calendrier est en lecture seule.", "selectTimeZone": "Sélectionner le fuseau horaire", "searchLocations": "Rechercher des lieux", "noLocationsFound": "Aucun lieu trouvé", + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", "deleteCalendarConfirmation": "Supprimer « {title} » ?", - "@deleteCalendarConfirmation": {"placeholders": {"title": {"type": "String"}}} + "@deleteCalendarConfirmation": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "L’échéance ne peut pas précéder le début.", + "taskStartDueTimeModeMismatch": "Définissez une heure pour le début et l’échéance, ou passez la tâche en journée entière.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Ne plus partager", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Nécessite une action", + "taskStatusInProcess": "En cours", + "taskStatusCompleted": "Terminé", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priorité", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "Aucun rappel", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Avant le début de la tâche", + "beforeTaskDue": "Avant l'échéance de la tâche", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Répéter chaque", + "repeatOn": "Repeat on", + "repeatEnd": "Arrêter la répétition", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "le {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} fois", + "repeatUntilSummary": "jusqu'au {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Masquer les sous-tâches", + "hideClosedSubtasks": "Masquer les sous-tâches fermées", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index a1fd102..8d9dcbb 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -1,14 +1,14 @@ { "@@locale": "hi", "appTitle": "BusyMax", - "connectGoogleAccount": "कैलेंडर और कार्य सिंक करने के लिए Google और Microsoft खाते कनेक्ट करें।", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Google अनुमति स्क्रीन पर, कैलेंडर और कार्य दोनों अनुमतियाँ चुनें।", "googlePermissionsRequiredRetry": "Google Calendar और Google Tasks की अनुमतियाँ आवश्यक हैं। फिर से कोशिश करें और दोनों चेकबॉक्स चुनें।", "finishSetup": "सेटअप पूरा करें", "continueSetup": "जारी रखें", "onboardingSetupTitle": "BusyMax सेट अप करें", "onboardingAccountsStepTitle": "खाते कनेक्ट करें", - "onboardingAccountsStepDescription": "वे सभी Google और Microsoft खाते जोड़ें जिन्हें आप उपयोग करना चाहते हैं। BusyMax प्रत्येक खाते के कैलेंडर, ईवेंट, कार्य सूचियाँ और कार्य सिंक करता है।", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "सिस्टम सेटिंग्स चुनें", "onboardingPreferencesStepDescription": "अपना शेड्यूल खोलने से पहले डेस्कटॉप व्यवहार, रिमाइंडर, सूचनाओं के विवरण का स्तर और दिखावट सेट करें।", "signInWithGoogle": "Google से साइन इन करें", @@ -180,7 +180,7 @@ "removingAccount": "खाता हटाया जा रहा है…", "removeAccountDescription": "सिंक करना बंद करें और इस डिवाइस से इस खाते का डेटा हटाएँ।", "removeAccountTitle": "BusyMax से {account} हटाएँ?", - "removeAccountConfirmation": "इससे कैश किए गए कार्य, कैलेंडर, ईवेंट, रिमाइंडर और लंबित ऑफ़लाइन बदलाव इस डिवाइस से मिट जाएँगे। सिंक न किए गए बदलाव खो जाएँगे। Google या Microsoft से कुछ भी नहीं मिटेगा।", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "इस Google खाते से BusyMax की पहुँच भी रद्द करें", "revokeGoogleAccessDescription": "दोबारा कनेक्ट करने से पहले आपको फिर से पहुँच देनी होगी।", "removeAccountAction": "खाता हटाएँ", @@ -273,10 +273,11 @@ "list": "सूची", "microsoftMoveUnsupported": "इस संस्करण में Microsoft To Do खातों के लिए सूचियों के बीच कार्य ले जाना समर्थित नहीं है।", "createSubtask": "उपकार्य बनाएँ", + "subtasks": "उपकार्य", "moveToTop": "सबसे ऊपर ले जाएँ", "deleteTask": "कार्य मिटाएँ", "newSubtask": "नया उपकार्य", - "deleteTaskConfirmation": "Google Tasks से “{title}” मिटाएँ?", + "deleteTaskConfirmation": "“{title}” मिटाएँ?", "metadata": "मेटाडेटा", "id": "आईडी", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "समय क्षेत्र चुनें", "searchLocations": "स्थान खोजें", "noLocationsFound": "कोई स्थान नहीं मिला", - "deleteCalendarConfirmation": "“{title}” मिटाएँ?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "“{title}” मिटाएँ?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "नियत समय प्रारंभ समय से पहले नहीं हो सकता।", + "taskStartDueTimeModeMismatch": "प्रारंभ और नियत समय दोनों सेट करें, या कार्य को पूरे दिन का बनाएँ।", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Unshare", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "In process", + "taskStatusCompleted": "Completed", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priority", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Repeat every", + "repeatOn": "Repeat on", + "repeatEnd": "End repeat", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} times", + "repeatUntilSummary": "until {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Hide subtasks", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 909038b..3855c4b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,14 +1,14 @@ { "@@locale": "it", "appTitle": "BusyMax", - "connectGoogleAccount": "Collega gli account Google e Microsoft per sincronizzare calendari e attività.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Nella schermata delle autorizzazioni di Google, seleziona sia l’autorizzazione per il calendario sia quella per le attività.", "googlePermissionsRequiredRetry": "Sono necessarie le autorizzazioni per Google Calendar e Google Tasks. Riprova e seleziona entrambe le caselle.", "finishSetup": "Completa la configurazione", "continueSetup": "Continua", "onboardingSetupTitle": "Configura BusyMax", "onboardingAccountsStepTitle": "Collega gli account", - "onboardingAccountsStepDescription": "Aggiungi tutti gli account Google e Microsoft che vuoi utilizzare. BusyMax sincronizza calendari, eventi, elenchi di attività e attività di ogni account.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Scegli le impostazioni di sistema", "onboardingPreferencesStepDescription": "Prima di aprire l’agenda, configura il comportamento sul desktop, i promemoria, il livello di dettaglio delle notifiche e l’aspetto.", "signInWithGoogle": "Accedi con Google", @@ -180,7 +180,7 @@ "removingAccount": "Rimozione account…", "removeAccountDescription": "Interrompi la sincronizzazione e rimuovi i dati di questo account dal dispositivo.", "removeAccountTitle": "Rimuovere {account} da BusyMax?", - "removeAccountConfirmation": "Questa azione elimina dal dispositivo attività, calendari, eventi e promemoria memorizzati nella cache, oltre alle modifiche offline in sospeso. Le modifiche non sincronizzate andranno perse. Non verrà eliminato nulla da Google o Microsoft.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Revoca anche l’accesso di BusyMax a questo account Google", "revokeGoogleAccessDescription": "Dovrai concedere nuovamente l’accesso prima di riconnetterti.", "removeAccountAction": "Rimuovi account", @@ -273,10 +273,11 @@ "list": "Elenco", "microsoftMoveUnsupported": "In questa versione non è possibile spostare attività tra elenchi negli account Microsoft To Do.", "createSubtask": "Crea sottoattività", + "subtasks": "Sottoattività", "moveToTop": "Sposta in cima", "deleteTask": "Elimina attività", "newSubtask": "Nuova sottoattività", - "deleteTaskConfirmation": "Eliminare «{title}» da Google Tasks?", + "deleteTaskConfirmation": "Eliminare «{title}»?", "metadata": "Metadati", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "Seleziona fuso orario", "searchLocations": "Cerca luoghi", "noLocationsFound": "Nessun luogo trovato", - "deleteCalendarConfirmation": "Eliminare «{title}»?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "Eliminare «{title}»?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "La scadenza non può precedere l'inizio.", + "taskStartDueTimeModeMismatch": "Imposta un orario sia per l'inizio sia per la scadenza, oppure rendi l'attività valida per l'intera giornata.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Rimuovi condivisione", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Richiede azione", + "taskStatusInProcess": "In corso", + "taskStatusCompleted": "Completato", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priorità", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "Nessun promemoria", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Prima dell'inizio dell'attività", + "beforeTaskDue": "Prima della scadenza del compito", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Scarta i promemoria", + "keepRelatedReminders": "Mantieni i promemoria", + "repeatEvery": "Ripeti ogni", + "repeatOn": "Repeat on", + "repeatEnd": "Termina ripetizione", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "il giorno {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} volte", + "repeatUntilSummary": "fino al {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Nascondi sottoattività", + "hideClosedSubtasks": "Nascondi sotto-attività chiuse", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 039f92b..ad6f633 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1,14 +1,14 @@ { "@@locale": "ja", "appTitle": "BusyMax", - "connectGoogleAccount": "Google と Microsoft のアカウントを接続して、カレンダーとタスクを同期します。", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Google の権限画面で、カレンダーとタスクの両方の権限を選択してください。", "googlePermissionsRequiredRetry": "Google カレンダーと Google Tasks の権限が必要です。もう一度試して、両方のチェックボックスを選択してください。", "finishSetup": "セットアップを完了", "continueSetup": "続行", "onboardingSetupTitle": "BusyMax をセットアップ", "onboardingAccountsStepTitle": "アカウントを接続", - "onboardingAccountsStepDescription": "使用するすべての Google アカウントと Microsoft アカウントを追加してください。BusyMax は各アカウントのカレンダー、予定、タスクリスト、タスクを同期します。", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "システム設定を選択", "onboardingPreferencesStepDescription": "スケジュールを開く前に、デスクトップでの動作、リマインダー、通知の詳細度、外観を設定します。", "signInWithGoogle": "Google でサインイン", @@ -180,7 +180,7 @@ "removingAccount": "アカウントを削除しています…", "removeAccountDescription": "同期を停止し、このアカウントのデータをこのデバイスから削除します。", "removeAccountTitle": "BusyMax から {account} を削除しますか?", - "removeAccountConfirmation": "このデバイスにキャッシュされたタスク、カレンダー、予定、リマインダー、保留中のオフライン変更が削除されます。同期されていない変更は失われます。Google または Microsoft から削除されるデータはありません。", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "この Google アカウントへの BusyMax のアクセス権も取り消す", "revokeGoogleAccessDescription": "再接続する前に、もう一度アクセスを許可する必要があります。", "removeAccountAction": "アカウントを削除", @@ -273,10 +273,11 @@ "list": "リスト", "microsoftMoveUnsupported": "このバージョンでは、Microsoft To Do アカウントのリスト間でタスクを移動できません。", "createSubtask": "サブタスクを作成", + "subtasks": "サブタスク", "moveToTop": "一番上に移動", "deleteTask": "タスクを削除", "newSubtask": "新しいサブタスク", - "deleteTaskConfirmation": "Google Tasks から「{title}」を削除しますか?", + "deleteTaskConfirmation": "「{title}」を削除しますか?", "metadata": "メタデータ", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "タイムゾーンを選択", "searchLocations": "場所を検索", "noLocationsFound": "場所が見つかりません", - "deleteCalendarConfirmation": "「{title}」を削除しますか?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "「{title}」を削除しますか?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "期限を開始より前に設定することはできません。", + "taskStartDueTimeModeMismatch": "開始と期限の両方に時刻を設定するか、タスクを終日にしてください。", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "共有を解除", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "アクションが必要", + "taskStatusInProcess": "進行中", + "taskStatusCompleted": "完了", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "優先度", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "毎日繰り返す", + "repeatOn": "Repeat on", + "repeatEnd": "繰り返し終了", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count}回", + "repeatUntilSummary": "{date}まで", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "サブタスクを非表示", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 4ce3f7b..a60b91e 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -1,14 +1,14 @@ { "@@locale": "ko", "appTitle": "BusyMax", - "connectGoogleAccount": "Google 및 Microsoft 계정을 연결하여 캘린더와 할 일을 동기화하세요.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Google 권한 화면에서 캘린더와 할 일 권한을 모두 선택하세요.", "googlePermissionsRequiredRetry": "Google Calendar 및 Google Tasks 권한이 필요합니다. 다시 시도하여 두 체크박스를 모두 선택하세요.", "finishSetup": "설정 완료", "continueSetup": "계속", "onboardingSetupTitle": "BusyMax 설정", "onboardingAccountsStepTitle": "계정 연결", - "onboardingAccountsStepDescription": "사용할 Google 및 Microsoft 계정을 모두 추가하세요. BusyMax는 각 계정의 캘린더, 일정, 할 일 목록 및 할 일을 동기화합니다.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "시스템 설정 선택", "onboardingPreferencesStepDescription": "일정을 열기 전에 데스크톱 동작, 미리 알림, 알림 세부 수준 및 화면 모양을 설정하세요.", "signInWithGoogle": "Google로 로그인", @@ -180,7 +180,7 @@ "removingAccount": "계정 삭제 중…", "removeAccountDescription": "동기화를 중지하고 이 기기에서 이 계정의 데이터를 삭제합니다.", "removeAccountTitle": "BusyMax에서 {account} 계정을 삭제할까요?", - "removeAccountConfirmation": "이 기기에서 캐시된 할 일, 캘린더, 일정, 미리 알림 및 보류 중인 오프라인 변경 사항이 삭제됩니다. 동기화되지 않은 변경 사항은 사라집니다. Google 또는 Microsoft에서는 아무것도 삭제되지 않습니다.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "이 Google 계정에 대한 BusyMax의 액세스 권한도 취소", "revokeGoogleAccessDescription": "다시 연결하기 전에 액세스 권한을 다시 부여해야 합니다.", "removeAccountAction": "계정 삭제", @@ -273,10 +273,11 @@ "list": "목록", "microsoftMoveUnsupported": "이 버전에서는 Microsoft To Do 계정의 목록 간에 할 일을 이동할 수 없습니다.", "createSubtask": "하위 할 일 만들기", + "subtasks": "하위 할 일", "moveToTop": "맨 위로 이동", "deleteTask": "할 일 삭제", "newSubtask": "새 하위 할 일", - "deleteTaskConfirmation": "Google Tasks에서 “{title}” 항목을 삭제할까요?", + "deleteTaskConfirmation": "“{title}” 항목을 삭제할까요?", "metadata": "메타데이터", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "시간대 선택", "searchLocations": "위치 검색", "noLocationsFound": "위치를 찾을 수 없습니다", - "deleteCalendarConfirmation": "“{title}” 캘린더를 삭제할까요?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "“{title}” 캘린더를 삭제할까요?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "마감은 시작보다 빠를 수 없습니다.", + "taskStartDueTimeModeMismatch": "시작과 마감에 모두 시간을 설정하거나 작업을 종일로 설정하세요.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "공유 해제", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "In process", + "taskStatusCompleted": "완료됨", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "우선 순위", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "반복 주기", + "repeatOn": "Repeat on", + "repeatEnd": "반복 종료", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count}회", + "repeatUntilSummary": "until {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Hide subtasks", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index cc54962..fa0d066 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,14 +1,14 @@ { "@@locale": "pt", "appTitle": "BusyMax", - "connectGoogleAccount": "Ligue contas Google e Microsoft para sincronizar calendários e tarefas.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "No ecrã de autorizações da Google, selecione as autorizações do Calendário e das Tarefas.", "googlePermissionsRequiredRetry": "As autorizações do Calendário Google e do Google Tasks são necessárias. Tente novamente e selecione ambas as caixas.", "finishSetup": "Concluir configuração", "continueSetup": "Continuar", "onboardingSetupTitle": "Configurar o BusyMax", "onboardingAccountsStepTitle": "Ligar contas", - "onboardingAccountsStepDescription": "Adicione todas as contas Google e Microsoft que pretende utilizar. O BusyMax sincroniza calendários, eventos, listas de tarefas e tarefas de cada conta.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Escolher definições do sistema", "onboardingPreferencesStepDescription": "Configure o comportamento da aplicação no ambiente de trabalho, os lembretes, o nível de detalhe das notificações e o aspeto antes de abrir a agenda.", "signInWithGoogle": "Iniciar sessão com a Google", @@ -180,7 +180,7 @@ "removingAccount": "A remover conta…", "removeAccountDescription": "Parar a sincronização e remover os dados desta conta deste dispositivo.", "removeAccountTitle": "Remover {account} do BusyMax?", - "removeAccountConfirmation": "Esta ação elimina deste dispositivo as tarefas, os calendários, os eventos, os lembretes e as alterações offline pendentes em cache. As alterações não sincronizadas serão perdidas. Nada será eliminado da Google ou da Microsoft.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Revogar também o acesso do BusyMax a esta conta Google", "revokeGoogleAccessDescription": "Terá de conceder acesso novamente antes de voltar a ligar a conta.", "removeAccountAction": "Remover conta", @@ -273,10 +273,11 @@ "list": "Lista", "microsoftMoveUnsupported": "Nesta versão, não é possível mover tarefas entre listas em contas Microsoft To Do.", "createSubtask": "Criar subtarefa", + "subtasks": "Subtarefas", "moveToTop": "Mover para o início", "deleteTask": "Eliminar tarefa", "newSubtask": "Nova subtarefa", - "deleteTaskConfirmation": "Eliminar «{title}» do Google Tasks?", + "deleteTaskConfirmation": "Eliminar «{title}»?", "metadata": "Metadados", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "Selecionar fuso horário", "searchLocations": "Pesquisar locais", "noLocationsFound": "Nenhum local encontrado", - "deleteCalendarConfirmation": "Eliminar «{title}»?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "Eliminar «{title}»?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "O prazo não pode ser anterior ao início.", + "taskStartDueTimeModeMismatch": "Defina horários para o início e o prazo, ou torne a tarefa de dia inteiro.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Cancelar partilha", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "Em andamento", + "taskStatusCompleted": "Concluída", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Prioridade", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Repetir a cada", + "repeatOn": "Repeat on", + "repeatEnd": "End repeat", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "em {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} vezes", + "repeatUntilSummary": "até {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Ocultar subtarefas", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index cc5400d..23369f2 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,14 +1,14 @@ { "@@locale": "ru", "appTitle": "BusyMax", - "connectGoogleAccount": "Подключите аккаунты Google и Microsoft, чтобы синхронизировать календари и задачи.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "На экране запроса доступа Google установите флажки «Google Календарь» и «Google Задачи».", "googlePermissionsRequiredRetry": "BusyMax требуется доступ к Google Календарю и Google Задачам. Повторите попытку и установите оба флажка.", "finishSetup": "Завершить настройку", "continueSetup": "Продолжить", "onboardingSetupTitle": "Настройка BusyMax", "onboardingAccountsStepTitle": "Подключите аккаунты", - "onboardingAccountsStepDescription": "Добавьте все аккаунты Google и Microsoft, которые хотите использовать. BusyMax синхронизирует календари, события, списки задач и задачи из каждого аккаунта.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Выберите системные параметры", "onboardingPreferencesStepDescription": "Настройте поведение приложения на рабочем столе, напоминания, уровень детализации уведомлений и внешний вид, прежде чем открыть расписание.", "signInWithGoogle": "Войти через Google", @@ -180,7 +180,7 @@ "removingAccount": "Удаление аккаунта…", "removeAccountDescription": "Остановить синхронизацию и удалить данные этого аккаунта с устройства.", "removeAccountTitle": "Удалить {account} из BusyMax?", - "removeAccountConfirmation": "С этого устройства будут удалены кэшированные задачи, календари, события, напоминания и локальные изменения, ожидающие синхронизации. Несинхронизированные изменения будут потеряны. В Google и Microsoft ничего не будет удалено.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Также отозвать у BusyMax доступ к этому аккаунту Google", "revokeGoogleAccessDescription": "Перед повторным подключением аккаунта потребуется снова предоставить доступ.", "removeAccountAction": "Удалить аккаунт", @@ -273,10 +273,11 @@ "list": "Список", "microsoftMoveUnsupported": "В этой версии перенос между списками для аккаунтов Microsoft To Do не поддерживается.", "createSubtask": "Создать подзадачу", + "subtasks": "Подзадачи", "moveToTop": "Переместить в самый верх", "deleteTask": "Удалить задачу", "newSubtask": "Новая подзадача", - "deleteTaskConfirmation": "Удалить «{title}» из Google Tasks?", + "deleteTaskConfirmation": "Удалить «{title}»?", "metadata": "Метаданные", "id": "Идентификатор", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "Выберите часовой пояс", "searchLocations": "Поиск города", "noLocationsFound": "Ничего не найдено", - "deleteCalendarConfirmation": "Удалить «{title}»?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "Удалить «{title}»?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Срок не может быть раньше начала.", + "taskStartDueTimeModeMismatch": "Укажите время начала и срока или сделайте задачу на весь день.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Закрыть доступ", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Требуется действие", + "taskStatusInProcess": "Выполянется", + "taskStatusCompleted": "Завершённые", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Приоритет", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "Напоминаний нет", + "editReminder": "Edit reminder", + "beforeTaskStarts": "До начала задачи", + "beforeTaskDue": "До срока выполнения задачи", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Отменить напоминания", + "keepRelatedReminders": "Сохранить напоминания", + "repeatEvery": "Повторять каждые", + "repeatOn": "Repeat on", + "repeatEnd": "Прекратить повтор", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "в {days}", + "repeatOnMonthDaysSummary": " {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "в {months}", + "repeatTimesSummary": "{count} раз", + "repeatUntilSummary": "до {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Дублировать задачу", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Скрыть вложенные задачи", + "hideClosedSubtasks": "Скрыть закрытые подзадачи", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 44e314f..fb69297 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1,14 +1,14 @@ { "@@locale": "vi", "appTitle": "BusyMax", - "connectGoogleAccount": "Kết nối tài khoản Google và Microsoft để đồng bộ lịch và công việc.", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "Trên màn hình cấp quyền của Google, hãy chọn cả quyền truy cập Lịch và Công việc.", "googlePermissionsRequiredRetry": "Cần có quyền truy cập Google Calendar và Google Tasks. Vui lòng thử lại và chọn cả hai hộp kiểm.", "finishSetup": "Hoàn tất thiết lập", "continueSetup": "Tiếp tục", "onboardingSetupTitle": "Thiết lập BusyMax", "onboardingAccountsStepTitle": "Kết nối tài khoản", - "onboardingAccountsStepDescription": "Thêm tất cả tài khoản Google và Microsoft bạn muốn sử dụng. BusyMax đồng bộ lịch, sự kiện, danh sách công việc và công việc từ mỗi tài khoản.", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "Chọn cài đặt hệ thống", "onboardingPreferencesStepDescription": "Thiết lập cách ứng dụng hoạt động trên máy tính, lời nhắc, mức độ chi tiết của thông báo và giao diện trước khi mở lịch biểu.", "signInWithGoogle": "Đăng nhập bằng Google", @@ -180,7 +180,7 @@ "removingAccount": "Đang xóa tài khoản…", "removeAccountDescription": "Dừng đồng bộ và xóa dữ liệu của tài khoản này khỏi thiết bị.", "removeAccountTitle": "Xóa {account} khỏi BusyMax?", - "removeAccountConfirmation": "Thao tác này sẽ xóa công việc, lịch, sự kiện, lời nhắc đã lưu trong bộ nhớ đệm và các thay đổi ngoại tuyến đang chờ khỏi thiết bị. Các thay đổi chưa đồng bộ sẽ bị mất. Không có dữ liệu nào bị xóa khỏi Google hoặc Microsoft.", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "Đồng thời thu hồi quyền truy cập của BusyMax vào tài khoản Google này", "revokeGoogleAccessDescription": "Bạn sẽ cần cấp lại quyền truy cập trước khi kết nối lại.", "removeAccountAction": "Xóa tài khoản", @@ -273,10 +273,11 @@ "list": "Danh sách", "microsoftMoveUnsupported": "Phiên bản này không hỗ trợ di chuyển công việc giữa các danh sách trong tài khoản Microsoft To Do.", "createSubtask": "Tạo công việc con", + "subtasks": "Công việc con", "moveToTop": "Chuyển lên đầu", "deleteTask": "Xóa công việc", "newSubtask": "Công việc con mới", - "deleteTaskConfirmation": "Xóa “{title}” khỏi Google Tasks?", + "deleteTaskConfirmation": "Xóa “{title}”?", "metadata": "Siêu dữ liệu", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "Chọn múi giờ", "searchLocations": "Tìm kiếm địa điểm", "noLocationsFound": "Không tìm thấy địa điểm", - "deleteCalendarConfirmation": "Xóa “{title}”?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "Xóa “{title}”?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "Hạn chót không được trước thời gian bắt đầu.", + "taskStartDueTimeModeMismatch": "Đặt giờ cho cả thời gian bắt đầu và hạn chót, hoặc đặt công việc là cả ngày.", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "Bỏ chia sẽ", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "Needs action", + "taskStatusInProcess": "In process", + "taskStatusCompleted": "Hoàn thành", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "Priority", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "No reminders", + "editReminder": "Edit reminder", + "beforeTaskStarts": "Before the task starts", + "beforeTaskDue": "Before the task is due", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "Discard reminders", + "keepRelatedReminders": "Keep reminders", + "repeatEvery": "Lặp lại mỗi", + "repeatOn": "Repeat on", + "repeatEnd": "Kết thúc lập lại", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "on {days}", + "repeatOnMonthDaysSummary": "on day {days}", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "in {months}", + "repeatTimesSummary": "{count} lần", + "repeatUntilSummary": "cho đến {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "Duplicate task", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "Hide subtasks", + "hideClosedSubtasks": "Hide closed subtasks", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 13a5ab4..6f2ed98 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,14 +1,14 @@ { "@@locale": "zh", "appTitle": "BusyMax", - "connectGoogleAccount": "连接 Google 和 Microsoft 帐户以同步日历和任务。", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "在 Google 权限页面上,同时选择日历和任务权限。", "googlePermissionsRequiredRetry": "必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。", "finishSetup": "完成设置", "continueSetup": "继续", "onboardingSetupTitle": "设置 BusyMax", "onboardingAccountsStepTitle": "连接帐户", - "onboardingAccountsStepDescription": "添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "选择系统设置", "onboardingPreferencesStepDescription": "打开日程前,请设置桌面行为、提醒、通知详细程度和外观。", "signInWithGoogle": "使用 Google 登录", @@ -180,7 +180,7 @@ "removingAccount": "正在移除帐户…", "removeAccountDescription": "停止同步并从此设备移除此帐户的数据。", "removeAccountTitle": "从 BusyMax 中移除 {account}?", - "removeAccountConfirmation": "这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "同时撤销 BusyMax 对此 Google 帐户的访问权限", "revokeGoogleAccessDescription": "重新连接之前,您需要再次授予访问权限。", "removeAccountAction": "移除帐户", @@ -273,10 +273,11 @@ "list": "列表", "microsoftMoveUnsupported": "此版本不支持在 Microsoft To Do 帐户的列表之间移动任务。", "createSubtask": "创建子任务", + "subtasks": "子任务", "moveToTop": "移到顶部", "deleteTask": "删除任务", "newSubtask": "新建子任务", - "deleteTaskConfirmation": "从 Google Tasks 中删除“{title}”?", + "deleteTaskConfirmation": "删除“{title}”?", "metadata": "元数据", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "选择时区", "searchLocations": "搜索地点", "noLocationsFound": "未找到地点", - "deleteCalendarConfirmation": "删除“{title}”?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "删除“{title}”?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "截止时间不能早于开始时间。", + "taskStartDueTimeModeMismatch": "请同时设置开始和截止时间,或将任务设为全天。", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "取消共享", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "需要操作", + "taskStatusInProcess": "处理中", + "taskStatusCompleted": "完成", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "优先级", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "无提醒", + "editReminder": "Edit reminder", + "beforeTaskStarts": "任务开始前", + "beforeTaskDue": "任务截止前", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "舍弃提醒", + "keepRelatedReminders": "保留提醒", + "repeatEvery": "重复每", + "repeatOn": "Repeat on", + "repeatEnd": "结束重复", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "在 {days}", + "repeatOnMonthDaysSummary": "在第 {days} 天", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "在 {months}", + "repeatTimesSummary": "{count}次", + "repeatUntilSummary": "至 {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "复制任务", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "隐藏子任务", + "hideClosedSubtasks": "隐藏关闭的子任务", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index 50e7461..fd64f10 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -1,14 +1,14 @@ { "@@locale": "zh_Hans", "appTitle": "BusyMax", - "connectGoogleAccount": "连接 Google 和 Microsoft 帐户以同步日历和任务。", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "在 Google 权限页面上,同时选择日历和任务权限。", "googlePermissionsRequiredRetry": "必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。", "finishSetup": "完成设置", "continueSetup": "继续", "onboardingSetupTitle": "设置 BusyMax", "onboardingAccountsStepTitle": "连接帐户", - "onboardingAccountsStepDescription": "添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "选择系统设置", "onboardingPreferencesStepDescription": "打开日程前,请设置桌面行为、提醒、通知详细程度和外观。", "signInWithGoogle": "使用 Google 登录", @@ -180,7 +180,7 @@ "removingAccount": "正在移除帐户…", "removeAccountDescription": "停止同步并从此设备移除此帐户的数据。", "removeAccountTitle": "从 BusyMax 中移除 {account}?", - "removeAccountConfirmation": "这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "同时撤销 BusyMax 对此 Google 帐户的访问权限", "revokeGoogleAccessDescription": "重新连接之前,您需要再次授予访问权限。", "removeAccountAction": "移除帐户", @@ -273,10 +273,11 @@ "list": "列表", "microsoftMoveUnsupported": "此版本不支持在 Microsoft To Do 帐户的列表之间移动任务。", "createSubtask": "创建子任务", + "subtasks": "子任务", "moveToTop": "移到顶部", "deleteTask": "删除任务", "newSubtask": "新建子任务", - "deleteTaskConfirmation": "从 Google Tasks 中删除“{title}”?", + "deleteTaskConfirmation": "删除“{title}”?", "metadata": "元数据", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "选择时区", "searchLocations": "搜索地点", "noLocationsFound": "未找到地点", - "deleteCalendarConfirmation": "删除“{title}”?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "删除“{title}”?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "截止时间不能早于开始时间。", + "taskStartDueTimeModeMismatch": "请同时设置开始和截止时间,或将任务设为全天。", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "取消共享", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "需要操作", + "taskStatusInProcess": "处理中", + "taskStatusCompleted": "完成", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "优先级", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "无提醒", + "editReminder": "Edit reminder", + "beforeTaskStarts": "任务开始前", + "beforeTaskDue": "任务截止前", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "舍弃提醒", + "keepRelatedReminders": "保留提醒", + "repeatEvery": "重复每", + "repeatOn": "Repeat on", + "repeatEnd": "结束重复", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "在 {days}", + "repeatOnMonthDaysSummary": "在第 {days} 天", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "在 {months}", + "repeatTimesSummary": "{count}次", + "repeatUntilSummary": "至 {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "复制任务", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "隐藏子任务", + "hideClosedSubtasks": "隐藏关闭的子任务", + "reminderUnit": "Unit" } diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index bb07de0..0bf004d 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -1,14 +1,14 @@ { "@@locale": "zh_Hant", "appTitle": "BusyMax", - "connectGoogleAccount": "連結 Google 和 Microsoft 帳戶以同步行事曆和待辦事項。", + "connectGoogleAccount": "Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.", "googlePermissionsConsentNotice": "在 Google 權限畫面中,同時選取行事曆和待辦事項權限。", "googlePermissionsRequiredRetry": "必須授予 Google 日曆和 Google Tasks 權限。請再試一次並勾選兩個核取方塊。", "finishSetup": "完成設定", "continueSetup": "繼續", "onboardingSetupTitle": "設定 BusyMax", "onboardingAccountsStepTitle": "連結帳戶", - "onboardingAccountsStepDescription": "新增您要使用的所有 Google 和 Microsoft 帳戶。BusyMax 會同步每個帳戶中的行事曆、活動、待辦清單和待辦事項。", + "onboardingAccountsStepDescription": "Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.", "onboardingPreferencesStepTitle": "選擇系統設定", "onboardingPreferencesStepDescription": "開啟行程前,請設定桌面行為、提醒、通知詳細程度和外觀。", "signInWithGoogle": "使用 Google 登入", @@ -180,7 +180,7 @@ "removingAccount": "正在移除帳戶…", "removeAccountDescription": "停止同步並從此裝置移除此帳戶的資料。", "removeAccountTitle": "要從 BusyMax 移除 {account} 嗎?", - "removeAccountConfirmation": "這會從此裝置刪除快取的待辦事項、行事曆、活動、提醒和待處理的離線變更。未同步的變更將會遺失。不會從 Google 或 Microsoft 刪除任何內容。", + "removeAccountConfirmation": "This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.", "revokeGoogleAccess": "同時撤銷 BusyMax 對此 Google 帳戶的存取權", "revokeGoogleAccessDescription": "重新連結前,您必須再次授予存取權。", "removeAccountAction": "移除帳戶", @@ -273,10 +273,11 @@ "list": "清單", "microsoftMoveUnsupported": "此版本不支援在 Microsoft To Do 帳戶的清單之間移動待辦事項。", "createSubtask": "建立子待辦事項", + "subtasks": "子待辦事項", "moveToTop": "移至頂端", "deleteTask": "刪除待辦事項", "newSubtask": "新增子待辦事項", - "deleteTaskConfirmation": "要從 Google Tasks 刪除「{title}」嗎?", + "deleteTaskConfirmation": "要刪除「{title}」嗎?", "metadata": "中繼資料", "id": "ID", "etag": "ETag", @@ -370,5 +371,201 @@ "selectTimeZone": "選擇時區", "searchLocations": "搜尋地點", "noLocationsFound": "找不到地點", - "deleteCalendarConfirmation": "要刪除「{title}」嗎?" + "requiredField": "This field is required.", + "providerConnectionDescription": "Connect calendars and tasks from one of these providers.", + "appleICloudProvider": "Apple iCloud Calendar", + "nextcloudProvider": "Nextcloud", + "appleICloudTasksProvider": "Apple iCloud", + "nextcloudTasksProvider": "Nextcloud Tasks", + "addAppleICloudAccount": "Add Apple iCloud Calendar account", + "addNextcloudAccount": "Add Nextcloud account", + "waitingForAppleICloud": "Connecting to Apple iCloud…", + "waitingForNextcloud": "Waiting for Nextcloud authorization…", + "connectAppleICloudTitle": "Connect Apple iCloud Calendar", + "appleAccountEmail": "Apple Account email", + "appleAppSpecificPassword": "App-specific password", + "appleAppSpecificPasswordHelp": "Create an app-specific password after enabling two-factor authentication for your Apple Account.", + "appleAppSpecificPasswordResetWarning": "Resetting your Apple Account password revokes app-specific passwords.", + "connectNextcloudTitle": "Connect Nextcloud", + "nextcloudServerUrl": "Nextcloud server or CalDAV address", + "nextcloudServerUrlHelp": "Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.", + "nextcloudBrowserAuthorizationHelp": "BusyMax will open your browser. Approve access there, then return to BusyMax.", + "connectAccountAction": "Connect", + "cancelAccountConnection": "Cancel connection", + "nextcloudAccountRemovedRevokeFailed": "The account was removed locally, but its Nextcloud app password could not be revoked.", + "davCachedOfflineNotice": "Calendar and task data is cached locally for offline use.", + "davReauthenticationRequired": "Reconnect this account to resume synchronization.", + "davTemporarilyUnavailable": "This account is temporarily unavailable.", + "davPermissionChanged": "Server permissions changed. Pending edits are paused.", + "davUnsupportedServer": "This server or provider profile is not supported.", + "collectionSettings": "Collections", + "calendarContent": "Calendar events", + "taskContent": "Tasks", + "readOnlySharedCollection": "Read-only or shared", + "pendingLocally": "Pending locally", + "conflictBlocked": "Blocked by conflict", + "authenticationBlocked": "Blocked until reconnect", + "operationFailed": "Operation failed", + "keepServerVersion": "Keep server version", + "reapplyLocalChange": "Review and reapply local change", + "duplicateLocalItem": "Duplicate as new item", + "deleteCalendarConfirmation": "要刪除「{title}」嗎?", + "davConnectionState": "Connection state", + "davConnected": "Connected", + "davConnecting": "Connecting…", + "davSignedOut": "Signed out", + "davLastSuccessfulSync": "Last successful sync: {time}", + "@davLastSuccessfulSync": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "davNeverSynced": "Not synchronized yet", + "refreshCollections": "Refresh collections", + "nextcloudServerHost": "Server: {host}", + "@nextcloudServerHost": { + "placeholders": { + "host": { + "type": "String" + } + } + }, + "collectionSupportsEvents": "Event calendar", + "collectionSupportsTasks": "Task list", + "collectionSupportsEventsAndTasks": "Events and tasks", + "writableCollection": "Writable", + "sharedCollection": "Shared", + "collectionLastSynced": "Last synchronized: {time}", + "@collectionLastSynced": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "collectionSyncError": "Sync issue: {code}", + "@collectionSyncError": { + "placeholders": { + "code": { + "type": "String" + } + } + }, + "syncConflicts": "Synchronization conflicts", + "remoteChangedAt": "Server changed: {time}", + "@remoteChangedAt": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "localPendingEdit": "Local edit: {summary}", + "@localPendingEdit": { + "placeholders": { + "summary": { + "type": "String" + } + } + }, + "conflictResolutionFailed": "The conflict could not be resolved.", + "recurringEventScope": "Recurring event scope", + "entireSeries": "Entire series", + "singleOccurrence": "This occurrence", + "thisAndFutureUnavailable": "This and future (not available)", + "chooseRecurringEventScope": "Choose whether this change applies to the entire series or only this occurrence.", + "taskDueBeforeStart": "到期時間不得早於開始時間。", + "taskStartDueTimeModeMismatch": "請同時設定開始與到期時間,或將工作設為全天。", + "taskListCreateFailed": "Could not create the task list: {error}", + "taskListRenameFailed": "Could not rename the task list: {error}", + "taskListDeleteFailed": "Could not delete the task list: {error}", + "unshare": "撤回分享", + "readOnlyTaskListCannotRename": "This task list is read-only and cannot be renamed.", + "taskListCannotDelete": "This task list cannot be deleted with your current permissions.", + "deleteTaskListConfirmation": "Delete \"{title}\" and all of its tasks?", + "unshareTaskListConfirmation": "Unshare \"{title}\" from this account?", + "taskStatus": "Status", + "taskStatusNone": "No status", + "taskStatusNeedsAction": "需要動作", + "taskStatusInProcess": "進行中", + "taskStatusCompleted": "完成", + "taskStatusCancelled": "Cancelled", + "completionPercent": "{percent}% completed", + "completionDate": "Completion date", + "priority": "優先", + "priorityNone": "No priority", + "priorityHighValue": "Priority {priority} · High", + "priorityMediumValue": "Priority {priority} · Medium", + "priorityLowValue": "Priority {priority} · Low", + "taskUrl": "URL", + "invalidTaskUrl": "Enter an absolute URL, including its scheme.", + "classification": "Classification", + "classificationPublic": "When shared, show the full task", + "classificationConfidential": "When shared, show only busy", + "classificationPrivate": "When shared, hide this task", + "pinTask": "Pin task", + "reminders": "Reminders", + "noReminders": "無提醒", + "editReminder": "Edit reminder", + "beforeTaskStarts": "在任務開始前", + "beforeTaskDue": "任務截止前", + "afterTaskStarts": "After the task starts", + "afterTaskDue": "After the task is due", + "relativeToTaskStart": "Relative to the task start date", + "relativeToTaskDue": "Relative to the task due date", + "reminderTimeOfDay": "Time of day", + "absoluteReminder": "At a date and time", + "reminderAmount": "Amount", + "reminderUnitSeconds": "Seconds", + "reminderUnitMinutes": "Minutes", + "reminderUnitHours": "Hours", + "reminderUnitDays": "Days", + "reminderUnitWeeks": "Weeks", + "reminderAtTaskStart": "At the task start", + "reminderAtTaskDue": "At the task due time", + "unsupportedReminder": "This reminder type is preserved but its time cannot be edited.", + "relatedRemindersTitle": "Keep related reminders?", + "relatedRemindersDescription": "This date has {count} related reminders. Keep them at their current date and time?", + "discardRelatedReminders": "捨棄提醒", + "keepRelatedReminders": "保留提醒", + "repeatEvery": "重複循環", + "repeatOn": "Repeat on", + "repeatEnd": "停止重複", + "repeatNever": "Never", + "repeatUntil": "On date", + "repeatAfter": "After a number of occurrences", + "repeatCount": "Occurrences", + "repeatDayOfMonth": "Days of month", + "repeatMonths": "Months", + "repeatOrdinal": "Weekday position", + "repeatSpecificDays": "Specific days", + "repeatFirst": "First", + "repeatSecond": "Second", + "repeatThird": "Third", + "repeatFourth": "Fourth", + "repeatFifth": "Fifth", + "repeatSecondToLast": "Second to last", + "repeatLast": "Last", + "repeatAnyDay": "Day", + "repeatWeekday": "Weekday", + "repeatWeekendDay": "Weekend day", + "repeatEveryDays": "Every {count} days", + "repeatEveryWeeks": "Every {count} weeks", + "repeatEveryMonths": "Every {count} months", + "repeatEveryYears": "Every {count} years", + "repeatOnDaysSummary": "於 {days}", + "repeatOnMonthDaysSummary": "於 {days} 天", + "repeatOnOrdinalSummary": "on the {ordinal} {days}", + "repeatInMonthsSummary": "在 {months}", + "repeatTimesSummary": "{count} 次", + "repeatUntilSummary": "到 {date}", + "unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.", + "duplicateTask": "再製任務", + "taskDuplicated": "Task duplicated.", + "taskDuplicateFailed": "Could not duplicate the task: {error}", + "hideSubtasks": "隱藏子工作項目", + "hideClosedSubtasks": "隱藏已關閉的子工作項目", + "reminderUnit": "Unit" } diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 54ff62e..01e9b62 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -141,7 +141,7 @@ abstract class AppLocalizations { /// No description provided for @connectGoogleAccount. /// /// In en, this message translates to: - /// **'Connect Google and Microsoft accounts to sync calendars and tasks.'** + /// **'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'** String get connectGoogleAccount; /// No description provided for @googlePermissionsConsentNotice. @@ -183,7 +183,7 @@ abstract class AppLocalizations { /// No description provided for @onboardingAccountsStepDescription. /// /// In en, this message translates to: - /// **'Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.'** + /// **'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'** String get onboardingAccountsStepDescription; /// No description provided for @onboardingPreferencesStepTitle. @@ -1215,7 +1215,7 @@ abstract class AppLocalizations { /// No description provided for @removeAccountConfirmation. /// /// In en, this message translates to: - /// **'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.'** + /// **'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'** String get removeAccountConfirmation; /// No description provided for @revokeGoogleAccess. @@ -1254,6 +1254,24 @@ abstract class AppLocalizations { /// **'New task list'** String get newTaskList; + /// No description provided for @taskListCreateFailed. + /// + /// In en, this message translates to: + /// **'Could not create the task list: {error}'** + String taskListCreateFailed(String error); + + /// No description provided for @taskListRenameFailed. + /// + /// In en, this message translates to: + /// **'Could not rename the task list: {error}'** + String taskListRenameFailed(String error); + + /// No description provided for @taskListDeleteFailed. + /// + /// In en, this message translates to: + /// **'Could not delete the task list: {error}'** + String taskListDeleteFailed(String error); + /// No description provided for @signInToViewTaskLists. /// /// In en, this message translates to: @@ -1296,6 +1314,24 @@ abstract class AppLocalizations { /// **'Delete list'** String get deleteList; + /// No description provided for @unshare. + /// + /// In en, this message translates to: + /// **'Unshare'** + String get unshare; + + /// No description provided for @readOnlyTaskListCannotRename. + /// + /// In en, this message translates to: + /// **'This task list is read-only and cannot be renamed.'** + String get readOnlyTaskListCannotRename; + + /// No description provided for @taskListCannotDelete. + /// + /// In en, this message translates to: + /// **'This task list cannot be deleted with your current permissions.'** + String get taskListCannotDelete; + /// No description provided for @builtInMicrosoftList. /// /// In en, this message translates to: @@ -1314,6 +1350,18 @@ abstract class AppLocalizations { /// **'Delete \"{title}\" from Google Tasks?'** String deleteListConfirmation(String title); + /// No description provided for @deleteTaskListConfirmation. + /// + /// In en, this message translates to: + /// **'Delete \"{title}\" and all of its tasks?'** + String deleteTaskListConfirmation(String title); + + /// No description provided for @unshareTaskListConfirmation. + /// + /// In en, this message translates to: + /// **'Unshare \"{title}\" from this account?'** + String unshareTaskListConfirmation(String title); + /// No description provided for @deleteEvent. /// /// In en, this message translates to: @@ -1536,6 +1584,126 @@ abstract class AppLocalizations { /// **'Done'** String get doneStatus; + /// No description provided for @taskStatus. + /// + /// In en, this message translates to: + /// **'Status'** + String get taskStatus; + + /// No description provided for @taskStatusNone. + /// + /// In en, this message translates to: + /// **'No status'** + String get taskStatusNone; + + /// No description provided for @taskStatusNeedsAction. + /// + /// In en, this message translates to: + /// **'Needs action'** + String get taskStatusNeedsAction; + + /// No description provided for @taskStatusInProcess. + /// + /// In en, this message translates to: + /// **'In process'** + String get taskStatusInProcess; + + /// No description provided for @taskStatusCompleted. + /// + /// In en, this message translates to: + /// **'Completed'** + String get taskStatusCompleted; + + /// No description provided for @taskStatusCancelled. + /// + /// In en, this message translates to: + /// **'Cancelled'** + String get taskStatusCancelled; + + /// No description provided for @completionPercent. + /// + /// In en, this message translates to: + /// **'{percent}% completed'** + String completionPercent(int percent); + + /// No description provided for @completionDate. + /// + /// In en, this message translates to: + /// **'Completion date'** + String get completionDate; + + /// No description provided for @priority. + /// + /// In en, this message translates to: + /// **'Priority'** + String get priority; + + /// No description provided for @priorityNone. + /// + /// In en, this message translates to: + /// **'No priority'** + String get priorityNone; + + /// No description provided for @priorityHighValue. + /// + /// In en, this message translates to: + /// **'Priority {priority} · High'** + String priorityHighValue(int priority); + + /// No description provided for @priorityMediumValue. + /// + /// In en, this message translates to: + /// **'Priority {priority} · Medium'** + String priorityMediumValue(int priority); + + /// No description provided for @priorityLowValue. + /// + /// In en, this message translates to: + /// **'Priority {priority} · Low'** + String priorityLowValue(int priority); + + /// No description provided for @taskUrl. + /// + /// In en, this message translates to: + /// **'URL'** + String get taskUrl; + + /// No description provided for @invalidTaskUrl. + /// + /// In en, this message translates to: + /// **'Enter an absolute URL, including its scheme.'** + String get invalidTaskUrl; + + /// No description provided for @classification. + /// + /// In en, this message translates to: + /// **'Classification'** + String get classification; + + /// No description provided for @classificationPublic. + /// + /// In en, this message translates to: + /// **'When shared, show the full task'** + String get classificationPublic; + + /// No description provided for @classificationConfidential. + /// + /// In en, this message translates to: + /// **'When shared, show only busy'** + String get classificationConfidential; + + /// No description provided for @classificationPrivate. + /// + /// In en, this message translates to: + /// **'When shared, hide this task'** + String get classificationPrivate; + + /// No description provided for @pinTask. + /// + /// In en, this message translates to: + /// **'Pin task'** + String get pinTask; + /// No description provided for @notes. /// /// In en, this message translates to: @@ -1608,6 +1776,156 @@ abstract class AppLocalizations { /// **'Add Reminder'** String get addReminder; + /// No description provided for @reminders. + /// + /// In en, this message translates to: + /// **'Reminders'** + String get reminders; + + /// No description provided for @noReminders. + /// + /// In en, this message translates to: + /// **'No reminders'** + String get noReminders; + + /// No description provided for @editReminder. + /// + /// In en, this message translates to: + /// **'Edit reminder'** + String get editReminder; + + /// No description provided for @beforeTaskStarts. + /// + /// In en, this message translates to: + /// **'Before the task starts'** + String get beforeTaskStarts; + + /// No description provided for @beforeTaskDue. + /// + /// In en, this message translates to: + /// **'Before the task is due'** + String get beforeTaskDue; + + /// No description provided for @afterTaskStarts. + /// + /// In en, this message translates to: + /// **'After the task starts'** + String get afterTaskStarts; + + /// No description provided for @afterTaskDue. + /// + /// In en, this message translates to: + /// **'After the task is due'** + String get afterTaskDue; + + /// No description provided for @relativeToTaskStart. + /// + /// In en, this message translates to: + /// **'Relative to the task start date'** + String get relativeToTaskStart; + + /// No description provided for @relativeToTaskDue. + /// + /// In en, this message translates to: + /// **'Relative to the task due date'** + String get relativeToTaskDue; + + /// No description provided for @reminderTimeOfDay. + /// + /// In en, this message translates to: + /// **'Time of day'** + String get reminderTimeOfDay; + + /// No description provided for @absoluteReminder. + /// + /// In en, this message translates to: + /// **'At a date and time'** + String get absoluteReminder; + + /// No description provided for @reminderAmount. + /// + /// In en, this message translates to: + /// **'Amount'** + String get reminderAmount; + + /// No description provided for @reminderUnit. + /// + /// In en, this message translates to: + /// **'Unit'** + String get reminderUnit; + + /// No description provided for @reminderUnitSeconds. + /// + /// In en, this message translates to: + /// **'Seconds'** + String get reminderUnitSeconds; + + /// No description provided for @reminderUnitMinutes. + /// + /// In en, this message translates to: + /// **'Minutes'** + String get reminderUnitMinutes; + + /// No description provided for @reminderUnitHours. + /// + /// In en, this message translates to: + /// **'Hours'** + String get reminderUnitHours; + + /// No description provided for @reminderUnitDays. + /// + /// In en, this message translates to: + /// **'Days'** + String get reminderUnitDays; + + /// No description provided for @reminderUnitWeeks. + /// + /// In en, this message translates to: + /// **'Weeks'** + String get reminderUnitWeeks; + + /// No description provided for @reminderAtTaskStart. + /// + /// In en, this message translates to: + /// **'At the task start'** + String get reminderAtTaskStart; + + /// No description provided for @reminderAtTaskDue. + /// + /// In en, this message translates to: + /// **'At the task due time'** + String get reminderAtTaskDue; + + /// No description provided for @unsupportedReminder. + /// + /// In en, this message translates to: + /// **'This reminder type is preserved but its time cannot be edited.'** + String get unsupportedReminder; + + /// No description provided for @relatedRemindersTitle. + /// + /// In en, this message translates to: + /// **'Keep related reminders?'** + String get relatedRemindersTitle; + + /// No description provided for @relatedRemindersDescription. + /// + /// In en, this message translates to: + /// **'This date has {count} related reminders. Keep them at their current date and time?'** + String relatedRemindersDescription(int count); + + /// No description provided for @discardRelatedReminders. + /// + /// In en, this message translates to: + /// **'Discard reminders'** + String get discardRelatedReminders; + + /// No description provided for @keepRelatedReminders. + /// + /// In en, this message translates to: + /// **'Keep reminders'** + String get keepRelatedReminders; + /// No description provided for @addGuest. /// /// In en, this message translates to: @@ -1674,6 +1992,198 @@ abstract class AppLocalizations { /// **'Yearly'** String get repeatYearly; + /// No description provided for @repeatEvery. + /// + /// In en, this message translates to: + /// **'Repeat every'** + String get repeatEvery; + + /// No description provided for @repeatOn. + /// + /// In en, this message translates to: + /// **'Repeat on'** + String get repeatOn; + + /// No description provided for @repeatEnd. + /// + /// In en, this message translates to: + /// **'End repeat'** + String get repeatEnd; + + /// No description provided for @repeatNever. + /// + /// In en, this message translates to: + /// **'Never'** + String get repeatNever; + + /// No description provided for @repeatUntil. + /// + /// In en, this message translates to: + /// **'On date'** + String get repeatUntil; + + /// No description provided for @repeatAfter. + /// + /// In en, this message translates to: + /// **'After a number of occurrences'** + String get repeatAfter; + + /// No description provided for @repeatCount. + /// + /// In en, this message translates to: + /// **'Occurrences'** + String get repeatCount; + + /// No description provided for @repeatDayOfMonth. + /// + /// In en, this message translates to: + /// **'Days of month'** + String get repeatDayOfMonth; + + /// No description provided for @repeatMonths. + /// + /// In en, this message translates to: + /// **'Months'** + String get repeatMonths; + + /// No description provided for @repeatOrdinal. + /// + /// In en, this message translates to: + /// **'Weekday position'** + String get repeatOrdinal; + + /// No description provided for @repeatSpecificDays. + /// + /// In en, this message translates to: + /// **'Specific days'** + String get repeatSpecificDays; + + /// No description provided for @repeatFirst. + /// + /// In en, this message translates to: + /// **'First'** + String get repeatFirst; + + /// No description provided for @repeatSecond. + /// + /// In en, this message translates to: + /// **'Second'** + String get repeatSecond; + + /// No description provided for @repeatThird. + /// + /// In en, this message translates to: + /// **'Third'** + String get repeatThird; + + /// No description provided for @repeatFourth. + /// + /// In en, this message translates to: + /// **'Fourth'** + String get repeatFourth; + + /// No description provided for @repeatFifth. + /// + /// In en, this message translates to: + /// **'Fifth'** + String get repeatFifth; + + /// No description provided for @repeatSecondToLast. + /// + /// In en, this message translates to: + /// **'Second to last'** + String get repeatSecondToLast; + + /// No description provided for @repeatLast. + /// + /// In en, this message translates to: + /// **'Last'** + String get repeatLast; + + /// No description provided for @repeatAnyDay. + /// + /// In en, this message translates to: + /// **'Day'** + String get repeatAnyDay; + + /// No description provided for @repeatWeekday. + /// + /// In en, this message translates to: + /// **'Weekday'** + String get repeatWeekday; + + /// No description provided for @repeatWeekendDay. + /// + /// In en, this message translates to: + /// **'Weekend day'** + String get repeatWeekendDay; + + /// No description provided for @repeatEveryDays. + /// + /// In en, this message translates to: + /// **'Every {count} days'** + String repeatEveryDays(int count); + + /// No description provided for @repeatEveryWeeks. + /// + /// In en, this message translates to: + /// **'Every {count} weeks'** + String repeatEveryWeeks(int count); + + /// No description provided for @repeatEveryMonths. + /// + /// In en, this message translates to: + /// **'Every {count} months'** + String repeatEveryMonths(int count); + + /// No description provided for @repeatEveryYears. + /// + /// In en, this message translates to: + /// **'Every {count} years'** + String repeatEveryYears(int count); + + /// No description provided for @repeatOnDaysSummary. + /// + /// In en, this message translates to: + /// **'on {days}'** + String repeatOnDaysSummary(String days); + + /// No description provided for @repeatOnMonthDaysSummary. + /// + /// In en, this message translates to: + /// **'on day {days}'** + String repeatOnMonthDaysSummary(String days); + + /// No description provided for @repeatOnOrdinalSummary. + /// + /// In en, this message translates to: + /// **'on the {ordinal} {days}'** + String repeatOnOrdinalSummary(String ordinal, String days); + + /// No description provided for @repeatInMonthsSummary. + /// + /// In en, this message translates to: + /// **'in {months}'** + String repeatInMonthsSummary(String months); + + /// No description provided for @repeatTimesSummary. + /// + /// In en, this message translates to: + /// **'{count} times'** + String repeatTimesSummary(int count); + + /// No description provided for @repeatUntilSummary. + /// + /// In en, this message translates to: + /// **'until {date}'** + String repeatUntilSummary(String date); + + /// No description provided for @unsupportedRecurrencePreserved. + /// + /// In en, this message translates to: + /// **'This recurrence rule uses options that this editor does not change.'** + String get unsupportedRecurrencePreserved; + /// No description provided for @importance. /// /// In en, this message translates to: @@ -1770,6 +2280,42 @@ abstract class AppLocalizations { /// **'Create subtask'** String get createSubtask; + /// No description provided for @subtasks. + /// + /// In en, this message translates to: + /// **'Subtasks'** + String get subtasks; + + /// No description provided for @duplicateTask. + /// + /// In en, this message translates to: + /// **'Duplicate task'** + String get duplicateTask; + + /// No description provided for @taskDuplicated. + /// + /// In en, this message translates to: + /// **'Task duplicated.'** + String get taskDuplicated; + + /// No description provided for @taskDuplicateFailed. + /// + /// In en, this message translates to: + /// **'Could not duplicate the task: {error}'** + String taskDuplicateFailed(String error); + + /// No description provided for @hideSubtasks. + /// + /// In en, this message translates to: + /// **'Hide subtasks'** + String get hideSubtasks; + + /// No description provided for @hideClosedSubtasks. + /// + /// In en, this message translates to: + /// **'Hide closed subtasks'** + String get hideClosedSubtasks; + /// No description provided for @moveToTop. /// /// In en, this message translates to: @@ -1791,7 +2337,7 @@ abstract class AppLocalizations { /// No description provided for @deleteTaskConfirmation. /// /// In en, this message translates to: - /// **'Delete \"{title}\" from Google Tasks?'** + /// **'Delete \"{title}\"?'** String deleteTaskConfirmation(String title); /// No description provided for @metadata. @@ -2352,6 +2898,390 @@ abstract class AppLocalizations { /// **'No locations found'** String get noLocationsFound; + /// No description provided for @requiredField. + /// + /// In en, this message translates to: + /// **'This field is required.'** + String get requiredField; + + /// No description provided for @providerConnectionDescription. + /// + /// In en, this message translates to: + /// **'Connect calendars and tasks from one of these providers.'** + String get providerConnectionDescription; + + /// No description provided for @appleICloudProvider. + /// + /// In en, this message translates to: + /// **'Apple iCloud Calendar'** + String get appleICloudProvider; + + /// No description provided for @nextcloudProvider. + /// + /// In en, this message translates to: + /// **'Nextcloud'** + String get nextcloudProvider; + + /// No description provided for @appleICloudTasksProvider. + /// + /// In en, this message translates to: + /// **'Apple iCloud'** + String get appleICloudTasksProvider; + + /// No description provided for @nextcloudTasksProvider. + /// + /// In en, this message translates to: + /// **'Nextcloud Tasks'** + String get nextcloudTasksProvider; + + /// No description provided for @addAppleICloudAccount. + /// + /// In en, this message translates to: + /// **'Add Apple iCloud Calendar account'** + String get addAppleICloudAccount; + + /// No description provided for @addNextcloudAccount. + /// + /// In en, this message translates to: + /// **'Add Nextcloud account'** + String get addNextcloudAccount; + + /// No description provided for @waitingForAppleICloud. + /// + /// In en, this message translates to: + /// **'Connecting to Apple iCloud…'** + String get waitingForAppleICloud; + + /// No description provided for @waitingForNextcloud. + /// + /// In en, this message translates to: + /// **'Waiting for Nextcloud authorization…'** + String get waitingForNextcloud; + + /// No description provided for @connectAppleICloudTitle. + /// + /// In en, this message translates to: + /// **'Connect Apple iCloud Calendar'** + String get connectAppleICloudTitle; + + /// No description provided for @appleAccountEmail. + /// + /// In en, this message translates to: + /// **'Apple Account email'** + String get appleAccountEmail; + + /// No description provided for @appleAppSpecificPassword. + /// + /// In en, this message translates to: + /// **'App-specific password'** + String get appleAppSpecificPassword; + + /// No description provided for @appleAppSpecificPasswordHelp. + /// + /// In en, this message translates to: + /// **'Create an app-specific password after enabling two-factor authentication for your Apple Account.'** + String get appleAppSpecificPasswordHelp; + + /// No description provided for @appleAppSpecificPasswordResetWarning. + /// + /// In en, this message translates to: + /// **'Resetting your Apple Account password revokes app-specific passwords.'** + String get appleAppSpecificPasswordResetWarning; + + /// No description provided for @connectNextcloudTitle. + /// + /// In en, this message translates to: + /// **'Connect Nextcloud'** + String get connectNextcloudTitle; + + /// No description provided for @nextcloudServerUrl. + /// + /// In en, this message translates to: + /// **'Nextcloud server or CalDAV address'** + String get nextcloudServerUrl; + + /// No description provided for @nextcloudServerUrlHelp. + /// + /// In en, this message translates to: + /// **'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'** + String get nextcloudServerUrlHelp; + + /// No description provided for @nextcloudBrowserAuthorizationHelp. + /// + /// In en, this message translates to: + /// **'BusyMax will open your browser. Approve access there, then return to BusyMax.'** + String get nextcloudBrowserAuthorizationHelp; + + /// No description provided for @connectAccountAction. + /// + /// In en, this message translates to: + /// **'Connect'** + String get connectAccountAction; + + /// No description provided for @cancelAccountConnection. + /// + /// In en, this message translates to: + /// **'Cancel connection'** + String get cancelAccountConnection; + + /// No description provided for @nextcloudAccountRemovedRevokeFailed. + /// + /// In en, this message translates to: + /// **'The account was removed locally, but its Nextcloud app password could not be revoked.'** + String get nextcloudAccountRemovedRevokeFailed; + + /// No description provided for @davCachedOfflineNotice. + /// + /// In en, this message translates to: + /// **'Calendar and task data is cached locally for offline use.'** + String get davCachedOfflineNotice; + + /// No description provided for @davReauthenticationRequired. + /// + /// In en, this message translates to: + /// **'Reconnect this account to resume synchronization.'** + String get davReauthenticationRequired; + + /// No description provided for @davTemporarilyUnavailable. + /// + /// In en, this message translates to: + /// **'This account is temporarily unavailable.'** + String get davTemporarilyUnavailable; + + /// No description provided for @davPermissionChanged. + /// + /// In en, this message translates to: + /// **'Server permissions changed. Pending edits are paused.'** + String get davPermissionChanged; + + /// No description provided for @davUnsupportedServer. + /// + /// In en, this message translates to: + /// **'This server or provider profile is not supported.'** + String get davUnsupportedServer; + + /// No description provided for @collectionSettings. + /// + /// In en, this message translates to: + /// **'Collections'** + String get collectionSettings; + + /// No description provided for @calendarContent. + /// + /// In en, this message translates to: + /// **'Calendar events'** + String get calendarContent; + + /// No description provided for @taskContent. + /// + /// In en, this message translates to: + /// **'Tasks'** + String get taskContent; + + /// No description provided for @readOnlySharedCollection. + /// + /// In en, this message translates to: + /// **'Read-only or shared'** + String get readOnlySharedCollection; + + /// No description provided for @pendingLocally. + /// + /// In en, this message translates to: + /// **'Pending locally'** + String get pendingLocally; + + /// No description provided for @conflictBlocked. + /// + /// In en, this message translates to: + /// **'Blocked by conflict'** + String get conflictBlocked; + + /// No description provided for @authenticationBlocked. + /// + /// In en, this message translates to: + /// **'Blocked until reconnect'** + String get authenticationBlocked; + + /// No description provided for @operationFailed. + /// + /// In en, this message translates to: + /// **'Operation failed'** + String get operationFailed; + + /// No description provided for @keepServerVersion. + /// + /// In en, this message translates to: + /// **'Keep server version'** + String get keepServerVersion; + + /// No description provided for @reapplyLocalChange. + /// + /// In en, this message translates to: + /// **'Review and reapply local change'** + String get reapplyLocalChange; + + /// No description provided for @duplicateLocalItem. + /// + /// In en, this message translates to: + /// **'Duplicate as new item'** + String get duplicateLocalItem; + + /// No description provided for @davConnectionState. + /// + /// In en, this message translates to: + /// **'Connection state'** + String get davConnectionState; + + /// No description provided for @davConnected. + /// + /// In en, this message translates to: + /// **'Connected'** + String get davConnected; + + /// No description provided for @davConnecting. + /// + /// In en, this message translates to: + /// **'Connecting…'** + String get davConnecting; + + /// No description provided for @davSignedOut. + /// + /// In en, this message translates to: + /// **'Signed out'** + String get davSignedOut; + + /// No description provided for @davLastSuccessfulSync. + /// + /// In en, this message translates to: + /// **'Last successful sync: {time}'** + String davLastSuccessfulSync(String time); + + /// No description provided for @davNeverSynced. + /// + /// In en, this message translates to: + /// **'Not synchronized yet'** + String get davNeverSynced; + + /// No description provided for @refreshCollections. + /// + /// In en, this message translates to: + /// **'Refresh collections'** + String get refreshCollections; + + /// No description provided for @nextcloudServerHost. + /// + /// In en, this message translates to: + /// **'Server: {host}'** + String nextcloudServerHost(String host); + + /// No description provided for @collectionSupportsEvents. + /// + /// In en, this message translates to: + /// **'Event calendar'** + String get collectionSupportsEvents; + + /// No description provided for @collectionSupportsTasks. + /// + /// In en, this message translates to: + /// **'Task list'** + String get collectionSupportsTasks; + + /// No description provided for @collectionSupportsEventsAndTasks. + /// + /// In en, this message translates to: + /// **'Events and tasks'** + String get collectionSupportsEventsAndTasks; + + /// No description provided for @writableCollection. + /// + /// In en, this message translates to: + /// **'Writable'** + String get writableCollection; + + /// No description provided for @sharedCollection. + /// + /// In en, this message translates to: + /// **'Shared'** + String get sharedCollection; + + /// No description provided for @collectionLastSynced. + /// + /// In en, this message translates to: + /// **'Last synchronized: {time}'** + String collectionLastSynced(String time); + + /// No description provided for @collectionSyncError. + /// + /// In en, this message translates to: + /// **'Sync issue: {code}'** + String collectionSyncError(String code); + + /// No description provided for @syncConflicts. + /// + /// In en, this message translates to: + /// **'Synchronization conflicts'** + String get syncConflicts; + + /// No description provided for @remoteChangedAt. + /// + /// In en, this message translates to: + /// **'Server changed: {time}'** + String remoteChangedAt(String time); + + /// No description provided for @localPendingEdit. + /// + /// In en, this message translates to: + /// **'Local edit: {summary}'** + String localPendingEdit(String summary); + + /// No description provided for @conflictResolutionFailed. + /// + /// In en, this message translates to: + /// **'The conflict could not be resolved.'** + String get conflictResolutionFailed; + + /// No description provided for @recurringEventScope. + /// + /// In en, this message translates to: + /// **'Recurring event scope'** + String get recurringEventScope; + + /// No description provided for @entireSeries. + /// + /// In en, this message translates to: + /// **'Entire series'** + String get entireSeries; + + /// No description provided for @singleOccurrence. + /// + /// In en, this message translates to: + /// **'This occurrence'** + String get singleOccurrence; + + /// No description provided for @thisAndFutureUnavailable. + /// + /// In en, this message translates to: + /// **'This and future (not available)'** + String get thisAndFutureUnavailable; + + /// No description provided for @chooseRecurringEventScope. + /// + /// In en, this message translates to: + /// **'Choose whether this change applies to the entire series or only this occurrence.'** + String get chooseRecurringEventScope; + + /// No description provided for @taskDueBeforeStart. + /// + /// In en, this message translates to: + /// **'Due must not be before start.'** + String get taskDueBeforeStart; + + /// No description provided for @taskStartDueTimeModeMismatch. + /// + /// In en, this message translates to: + /// **'Set times for both start and due, or make the task all day.'** + String get taskStartDueTimeModeMismatch; + /// No description provided for @deleteCalendarConfirmation. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index d403251..e9b5c7f 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -17,7 +17,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get connectGoogleAccount => - 'اربط حسابات Google وMicrosoft لمزامنة التقويمات والمهام.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'أضف جميع حسابات Google وMicrosoft التي تريد استخدامها. يزامن BusyMax التقويمات والأحداث وقوائم المهام والمهام من كل حساب.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'اختيار إعدادات النظام'; @@ -623,7 +623,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String get removeAccountConfirmation => - 'سيؤدي ذلك إلى حذف المهام والتقويمات والأحداث والتذكيرات والتغييرات غير المتصلة المعلّقة المخزّنة مؤقتًا من هذا الجهاز. ستُفقد التغييرات غير المتزامنة. لن يُحذف أي شيء من Google أو Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -646,6 +646,21 @@ class AppLocalizationsAr extends AppLocalizations { @override String get newTaskList => 'قائمة مهام جديدة'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'سجّل الدخول لعرض قوائم المهام.'; @@ -667,6 +682,17 @@ class AppLocalizationsAr extends AppLocalizations { @override String get deleteList => 'حذف القائمة'; + @override + String get unshare => 'إلغاء المشاركة'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'مدمجة'; @@ -679,6 +705,16 @@ class AppLocalizationsAr extends AppLocalizations { return 'حذف «⁨$title⁩» من Google Tasks؟'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'حذف الحدث'; @@ -800,6 +836,74 @@ class AppLocalizationsAr extends AppLocalizations { @override String get doneStatus => 'منجزة'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'تحتاج إلى إجراء'; + + @override + String get taskStatusInProcess => 'تحت الإجراء'; + + @override + String get taskStatusCompleted => 'مُكتمِل'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'الأولوية'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'ملاحظات'; @@ -836,6 +940,84 @@ class AppLocalizationsAr extends AppLocalizations { @override String get addReminder => 'إضافة تذكير'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'لا توجد أي تذكيرات'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'قبل أن تبدأ المهمة'; + + @override + String get beforeTaskDue => 'قبل اكتمال المهمة'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'إضافة مدعو'; @@ -869,6 +1051,123 @@ class AppLocalizationsAr extends AppLocalizations { @override String get repeatYearly => 'سنويًا'; + @override + String get repeatEvery => 'تكرار كل'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'نهاية التكرار'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'في يوم $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count مرات'; + } + + @override + String repeatUntilSummary(String date) { + return 'until $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'الأهمية'; @@ -918,6 +1217,26 @@ class AppLocalizationsAr extends AppLocalizations { @override String get createSubtask => 'إنشاء مهمة فرعية'; + @override + String get subtasks => 'مهام فرعية'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'إخْفِ المهام الفرعية'; + + @override + String get hideClosedSubtasks => 'إخف المهام الفرعية المغلقة'; + @override String get moveToTop => 'نقل إلى الأعلى'; @@ -929,7 +1248,7 @@ class AppLocalizationsAr extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'حذف «⁨$title⁩» من Google Tasks؟'; + return 'حذف «⁨$title⁩»؟'; } @override @@ -1256,6 +1575,223 @@ class AppLocalizationsAr extends AppLocalizations { @override String get noLocationsFound => 'لم يتم العثور على مواقع'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'يجب ألا يكون موعد الاستحقاق قبل وقت البدء.'; + + @override + String get taskStartDueTimeModeMismatch => + 'عيّن وقتًا لكل من البدء والاستحقاق، أو اجعل المهمة طوال اليوم.'; + @override String deleteCalendarConfirmation(String title) { return 'حذف «⁨$title⁩»؟'; diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index f5f8a29..871492a 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -17,7 +17,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get connectGoogleAccount => - 'Verbinden Sie Google- und Microsoft-Konten, um Kalender und Aufgaben zu synchronisieren.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Fügen Sie alle Google- und Microsoft-Konten hinzu, die Sie verwenden möchten. BusyMax synchronisiert Kalender, Termine, Aufgabenlisten und Aufgaben aus jedem Konto.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Systemeinstellungen wählen'; @@ -613,7 +613,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get removeAccountConfirmation => - 'Dadurch werden zwischengespeicherte Aufgaben, Kalender, Termine, Erinnerungen und ausstehende Offline-Änderungen von diesem Gerät gelöscht. Nicht synchronisierte Änderungen gehen verloren. Bei Google oder Microsoft wird nichts gelöscht.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -637,6 +637,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get newTaskList => 'Neue Aufgabenliste'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Melden Sie sich an, um Aufgabenlisten zu sehen.'; @@ -659,6 +674,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get deleteList => 'Liste löschen'; + @override + String get unshare => 'Freigabe aufheben'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Integriert'; @@ -671,6 +697,16 @@ class AppLocalizationsDe extends AppLocalizations { return '\"$title\" aus Google Tasks löschen?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Termin löschen'; @@ -796,6 +832,74 @@ class AppLocalizationsDe extends AppLocalizations { @override String get doneStatus => 'Erledigt'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Handlungsbedarf'; + + @override + String get taskStatusInProcess => 'In Bearbeitung'; + + @override + String get taskStatusCompleted => 'Fertiggestellt'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priorität'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Notizen'; @@ -832,6 +936,84 @@ class AppLocalizationsDe extends AppLocalizations { @override String get addReminder => 'Erinnerung hinzufügen'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'Keine Erinnerungen'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Bevor die Aufgabe startet'; + + @override + String get beforeTaskDue => 'Bevor die Aufgabe fällig ist'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Erinnerungen verwerfen'; + + @override + String get keepRelatedReminders => 'Erinnerungen behalten'; + @override String get addGuest => 'Gast hinzufügen'; @@ -865,6 +1047,123 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeatYearly => 'Jährlich'; + @override + String get repeatEvery => 'Wiederhole jeden'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Wiederholung beenden'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'an $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'am Tag $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count mal'; + } + + @override + String repeatUntilSummary(String date) { + return 'bis $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Wichtigkeit'; @@ -914,6 +1213,26 @@ class AppLocalizationsDe extends AppLocalizations { @override String get createSubtask => 'Unteraufgabe erstellen'; + @override + String get subtasks => 'Unteraufgaben'; + + @override + String get duplicateTask => 'Aufgabe duplizieren'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Teilaufgaben ausblenden'; + + @override + String get hideClosedSubtasks => 'Geschlossene Teilaufgaben ausblenden'; + @override String get moveToTop => 'Ganz nach oben verschieben'; @@ -925,7 +1244,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return '\"$title\" aus Google Tasks löschen?'; + return '\"$title\" löschen?'; } @override @@ -1253,6 +1572,224 @@ class AppLocalizationsDe extends AppLocalizations { @override String get noLocationsFound => 'Keine Orte gefunden'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => + 'Die Fälligkeit darf nicht vor dem Beginn liegen.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Lege für Beginn und Fälligkeit jeweils eine Uhrzeit fest oder mache die Aufgabe ganztägig.'; + @override String deleteCalendarConfirmation(String title) { return '\"$title\" löschen?'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 308a1a5..47c31b5 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -17,7 +17,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get connectGoogleAccount => - 'Connect Google and Microsoft accounts to sync calendars and tasks.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Add all Google and Microsoft accounts you want to use. BusyMax syncs calendars, events, task lists, and tasks from each account.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Choose system settings'; @@ -609,7 +609,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get removeAccountConfirmation => - 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Nothing will be deleted from Google or Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -633,6 +633,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get newTaskList => 'New task list'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Sign in to view task lists.'; @@ -654,6 +669,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get deleteList => 'Delete list'; + @override + String get unshare => 'Unshare'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Built-in'; @@ -666,6 +692,16 @@ class AppLocalizationsEn extends AppLocalizations { return 'Delete \"$title\" from Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Delete Event'; @@ -788,6 +824,74 @@ class AppLocalizationsEn extends AppLocalizations { @override String get doneStatus => 'Done'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'In process'; + + @override + String get taskStatusCompleted => 'Completed'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priority'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Notes'; @@ -824,6 +928,84 @@ class AppLocalizationsEn extends AppLocalizations { @override String get addReminder => 'Add Reminder'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Add Guest'; @@ -857,6 +1039,123 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeatYearly => 'Yearly'; + @override + String get repeatEvery => 'Repeat every'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'End repeat'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count times'; + } + + @override + String repeatUntilSummary(String date) { + return 'until $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Importance'; @@ -906,6 +1205,26 @@ class AppLocalizationsEn extends AppLocalizations { @override String get createSubtask => 'Create subtask'; + @override + String get subtasks => 'Subtasks'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Hide subtasks'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => 'Move to top'; @@ -917,7 +1236,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Delete \"$title\" from Google Tasks?'; + return 'Delete \"$title\"?'; } @override @@ -1237,6 +1556,223 @@ class AppLocalizationsEn extends AppLocalizations { @override String get noLocationsFound => 'No locations found'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'Due must not be before start.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Set times for both start and due, or make the task all day.'; + @override String deleteCalendarConfirmation(String title) { return 'Delete \"$title\"?'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index d15f0b3..ac1dd30 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -17,7 +17,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get connectGoogleAccount => - 'Conecta cuentas de Google y Microsoft para sincronizar calendarios y tareas.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Añade todas las cuentas de Google y Microsoft que quieras usar. BusyMax sincroniza calendarios, eventos, listas de tareas y tareas de cada cuenta.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Elegir ajustes del sistema'; @@ -615,7 +615,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get removeAccountConfirmation => - 'Esto elimina de este dispositivo las tareas, los calendarios, los eventos, los recordatorios y los cambios sin conexión pendientes almacenados en caché. Los cambios no sincronizados se perderán. No se eliminará nada de Google ni Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -639,6 +639,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String get newTaskList => 'Nueva lista de tareas'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Inicia sesión para ver las listas de tareas.'; @@ -661,6 +676,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get deleteList => 'Eliminar lista'; + @override + String get unshare => 'No compartir'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Integrada'; @@ -673,6 +699,16 @@ class AppLocalizationsEs extends AppLocalizations { return '¿Eliminar \"$title\" de Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Eliminar evento'; @@ -797,6 +833,74 @@ class AppLocalizationsEs extends AppLocalizations { @override String get doneStatus => 'Completada'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Necesita una acción'; + + @override + String get taskStatusInProcess => 'En proceso'; + + @override + String get taskStatusCompleted => 'Completada'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Prioridad'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Notas'; @@ -833,6 +937,84 @@ class AppLocalizationsEs extends AppLocalizations { @override String get addReminder => 'Añadir recordatorio'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'Sin recordatorio'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Antes de empezar la tarea'; + + @override + String get beforeTaskDue => 'Antes de terminar la tarea'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Descartar recordatorios'; + + @override + String get keepRelatedReminders => 'Mantener recordatorios'; + @override String get addGuest => 'Añadir invitado'; @@ -866,6 +1048,123 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeatYearly => 'Anual'; + @override + String get repeatEvery => 'Repetir cada'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Finalizar repetición'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'dentro de $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'en día $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'en $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count veces'; + } + + @override + String repeatUntilSummary(String date) { + return 'hasta el $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Importancia'; @@ -915,6 +1214,26 @@ class AppLocalizationsEs extends AppLocalizations { @override String get createSubtask => 'Crear subtarea'; + @override + String get subtasks => 'Subtareas'; + + @override + String get duplicateTask => 'Duplicar tarea'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Ocultar subtareas'; + + @override + String get hideClosedSubtasks => 'Ocultar subtareas cerradas'; + @override String get moveToTop => 'Mover al principio'; @@ -926,7 +1245,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return '¿Eliminar \"$title\" de Google Tasks?'; + return '¿Eliminar \"$title\"?'; } @override @@ -1254,6 +1573,224 @@ class AppLocalizationsEs extends AppLocalizations { @override String get noLocationsFound => 'No se encontraron ubicaciones'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => + 'El vencimiento no puede ser anterior al inicio.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Define una hora tanto para el inicio como para el vencimiento, o configura la tarea para todo el día.'; + @override String deleteCalendarConfirmation(String title) { return '¿Eliminar \"$title\"?'; diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart index efc461f..2fbac7f 100644 --- a/lib/l10n/generated/app_localizations_et.dart +++ b/lib/l10n/generated/app_localizations_et.dart @@ -17,7 +17,7 @@ class AppLocalizationsEt extends AppLocalizations { @override String get connectGoogleAccount => - 'Ühendage Google\'i ja Microsofti kontod kalendrite ja ülesannete sünkroonimiseks.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsEt extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Lisage kõik Google\'i ja Microsofti kontod, mida soovite kasutada. BusyMax sünkroonib iga konto kalendrid, sündmused, ülesandeloendid ja ülesanded.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Süsteemiseadete valimine'; @@ -611,7 +611,7 @@ class AppLocalizationsEt extends AppLocalizations { @override String get removeAccountConfirmation => - 'See kustutab seadmest vahemällu salvestatud ülesanded, kalendrid, sündmused, meeldetuletused ja sünkroonimist ootavad võrguühenduseta muudatused. Sünkroonimata muudatused lähevad kaotsi. Google\'ist ega Microsoftist midagi ei kustutata.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -635,6 +635,21 @@ class AppLocalizationsEt extends AppLocalizations { @override String get newTaskList => 'Uus ülesandeloend'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Ülesandeloendite vaatamiseks logige sisse.'; @@ -658,6 +673,17 @@ class AppLocalizationsEt extends AppLocalizations { @override String get deleteList => 'Kustuta loend'; + @override + String get unshare => 'Unshare'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Sisseehitatud'; @@ -670,6 +696,16 @@ class AppLocalizationsEt extends AppLocalizations { return 'Kas kustutada „$title” Google Tasksist?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Kustuta sündmus'; @@ -794,6 +830,74 @@ class AppLocalizationsEt extends AppLocalizations { @override String get doneStatus => 'Valmis'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'In process'; + + @override + String get taskStatusCompleted => 'Completed'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priority'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Märkmed'; @@ -830,6 +934,84 @@ class AppLocalizationsEt extends AppLocalizations { @override String get addReminder => 'Lisa meeldetuletus'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Lisa külaline'; @@ -863,6 +1045,123 @@ class AppLocalizationsEt extends AppLocalizations { @override String get repeatYearly => 'Iga aasta'; + @override + String get repeatEvery => 'Repeat every'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'End repeat'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count times'; + } + + @override + String repeatUntilSummary(String date) { + return 'until $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Tähtsus'; @@ -912,6 +1211,26 @@ class AppLocalizationsEt extends AppLocalizations { @override String get createSubtask => 'Loo alamülesanne'; + @override + String get subtasks => 'Alamülesanded'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Hide subtasks'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => 'Teisalda kõige üles'; @@ -923,7 +1242,7 @@ class AppLocalizationsEt extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Kas kustutada „$title” Google Tasksist?'; + return 'Kas kustutada „$title”?'; } @override @@ -1244,6 +1563,223 @@ class AppLocalizationsEt extends AppLocalizations { @override String get noLocationsFound => 'Asukohti ei leitud'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'Tähtaeg ei tohi olla enne algust.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Määra nii algus- kui ka tähtaja kellaaeg või tee ülesanne kogu päeva ülesandeks.'; + @override String deleteCalendarConfirmation(String title) { return 'Kas kustutada „$title”?'; diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index abb3d3d..d264e4e 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -17,7 +17,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get connectGoogleAccount => - 'حساب‌های Google و Microsoft را متصل کنید تا تقویم‌ها و کارها همگام شوند.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'همهٔ حساب‌های Google و Microsoft موردنظرتان را اضافه کنید. BusyMax تقویم‌ها، رویدادها، فهرست‌های کار و کارهای هر حساب را همگام می‌کند.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'انتخاب تنظیمات سیستم'; @@ -630,7 +630,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String get removeAccountConfirmation => - 'با این کار، کارها، تقویم‌ها، رویدادها، یادآورها و تغییرات آفلاین در انتظار از حافظهٔ نهان این دستگاه حذف می‌شوند. تغییرات همگام‌نشده از دست می‌روند. هیچ چیزی از Google یا Microsoft حذف نمی‌شود.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -653,6 +653,21 @@ class AppLocalizationsFa extends AppLocalizations { @override String get newTaskList => 'فهرست کار جدید'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'برای دیدن فهرست‌های کار وارد شوید.'; @@ -674,6 +689,17 @@ class AppLocalizationsFa extends AppLocalizations { @override String get deleteList => 'حذف فهرست'; + @override + String get unshare => 'لغو اشتراک‌گذاری'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'داخلی'; @@ -686,6 +712,16 @@ class AppLocalizationsFa extends AppLocalizations { return '«⁨$title⁩» از Google Tasks حذف شود؟'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'حذف رویداد'; @@ -810,6 +846,74 @@ class AppLocalizationsFa extends AppLocalizations { @override String get doneStatus => 'انجام‌شده'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'نیاز به اقدام'; + + @override + String get taskStatusInProcess => 'در حال انجام'; + + @override + String get taskStatusCompleted => 'انجام‌شده'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'اولویت'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'یادداشت‌ها'; @@ -846,6 +950,84 @@ class AppLocalizationsFa extends AppLocalizations { @override String get addReminder => 'افزودن یادآور'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'بدون یادآوری'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'قبل از شروع کار'; + + @override + String get beforeTaskDue => 'قبل از موعد انجام کار'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'حذف یادآوری‌ها'; + + @override + String get keepRelatedReminders => 'نگه‌داشتن یادآوری‌ها'; + @override String get addGuest => 'افزودن مهمان'; @@ -879,6 +1061,123 @@ class AppLocalizationsFa extends AppLocalizations { @override String get repeatYearly => 'سالانه'; + @override + String get repeatEvery => 'تکرار هر'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'پایان تکرار'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'در $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'در روز $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'در $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count بار'; + } + + @override + String repeatUntilSummary(String date) { + return 'تا $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'اهمیت'; @@ -928,6 +1227,26 @@ class AppLocalizationsFa extends AppLocalizations { @override String get createSubtask => 'ایجاد زیرکار'; + @override + String get subtasks => 'زیرکارها'; + + @override + String get duplicateTask => 'تکراری‌سازی وظیفه'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'مخفی‌سازی زیروظیفه‌ها'; + + @override + String get hideClosedSubtasks => 'مخفی‌سازی زیروظیفه‌های بسته‌شده'; + @override String get moveToTop => 'انتقال به بالاترین جایگاه'; @@ -939,7 +1258,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return '«⁨$title⁩» از Google Tasks حذف شود؟'; + return '«⁨$title⁩» حذف شود؟'; } @override @@ -1279,6 +1598,223 @@ class AppLocalizationsFa extends AppLocalizations { @override String get noLocationsFound => 'مکانی پیدا نشد'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'زمان سررسید نباید پیش از زمان شروع باشد.'; + + @override + String get taskStartDueTimeModeMismatch => + 'برای شروع و سررسید هر دو زمان تعیین کنید، یا کار را تمام‌روز کنید.'; + @override String deleteCalendarConfirmation(String title) { return '«⁨$title⁩» حذف شود؟'; diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 14379a2..59d2030 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -17,7 +17,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get connectGoogleAccount => - 'Yhdistä Google- ja Microsoft-tilit kalenterien ja tehtävien synkronointia varten.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Lisää kaikki haluamasi Google- ja Microsoft-tilit. BusyMax synkronoi kunkin tilin kalenterit, tapahtumat, tehtäväluettelot ja tehtävät.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Valitse järjestelmäasetukset'; @@ -614,7 +614,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String get removeAccountConfirmation => - 'Tämä poistaa välimuistissa olevat tehtävät, kalenterit, tapahtumat, muistutukset ja odottavat offline-muutokset tältä laitteelta. Synkronoimattomat muutokset menetetään. Mitään ei poisteta Googlesta tai Microsoftista.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -638,6 +638,21 @@ class AppLocalizationsFi extends AppLocalizations { @override String get newTaskList => 'Uusi tehtäväluettelo'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Kirjaudu sisään nähdäksesi tehtäväluettelot.'; @@ -661,6 +676,17 @@ class AppLocalizationsFi extends AppLocalizations { @override String get deleteList => 'Poista luettelo'; + @override + String get unshare => 'Lopeta jakaminen'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Sisäänrakennettu'; @@ -673,6 +699,16 @@ class AppLocalizationsFi extends AppLocalizations { return 'Poistetaanko \"$title\" Google Tasksista?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Poista tapahtuma'; @@ -797,6 +833,74 @@ class AppLocalizationsFi extends AppLocalizations { @override String get doneStatus => 'Valmis'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Vaatii toimenpiteitä'; + + @override + String get taskStatusInProcess => 'Käsittelyssä'; + + @override + String get taskStatusCompleted => 'Valmiina'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Tärkeys'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Muistiinpanot'; @@ -833,6 +937,84 @@ class AppLocalizationsFi extends AppLocalizations { @override String get addReminder => 'Lisää muistutus'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Lisää vieras'; @@ -866,6 +1048,123 @@ class AppLocalizationsFi extends AppLocalizations { @override String get repeatYearly => 'Vuosittain'; + @override + String get repeatEvery => 'Toista joka'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Lopeta toisto'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'päivänä $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count kertaa'; + } + + @override + String repeatUntilSummary(String date) { + return '$date asti'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Tärkeys'; @@ -915,6 +1214,26 @@ class AppLocalizationsFi extends AppLocalizations { @override String get createSubtask => 'Luo alitehtävä'; + @override + String get subtasks => 'Alitehtävät'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Piilota alitehtävät'; + + @override + String get hideClosedSubtasks => 'Piilota suljetut alitehtävät'; + @override String get moveToTop => 'Siirrä ylimmäksi'; @@ -926,7 +1245,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Poistetaanko \"$title\" Google Tasksista?'; + return 'Poistetaanko \"$title\"?'; } @override @@ -1249,6 +1568,223 @@ class AppLocalizationsFi extends AppLocalizations { @override String get noLocationsFound => 'Sijainteja ei löytynyt'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'Määräaika ei saa olla ennen alkamisaikaa.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Aseta kellonaika sekä alkamiselle että määräajalle tai tee tehtävästä koko päivän tehtävä.'; + @override String deleteCalendarConfirmation(String title) { return 'Poistetaanko \"$title\"?'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 0a38e0c..dfa569a 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -17,7 +17,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get connectGoogleAccount => - 'Connectez des comptes Google et Microsoft pour synchroniser calendriers et tâches.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Ajoutez tous les comptes Google et Microsoft que vous voulez utiliser. BusyMax synchronise les calendriers, événements, listes de tâches et tâches de chaque compte.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Choisir les paramètres système'; @@ -613,7 +613,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get removeAccountConfirmation => - 'Cette action supprime de cet appareil les tâches, calendriers, événements, rappels et modifications hors ligne en attente mis en cache. Les modifications non synchronisées seront perdues. Aucune donnée ne sera supprimée de Google ou Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -637,6 +637,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get newTaskList => 'Nouvelle liste de tâches'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Connectez-vous pour voir les listes de tâches.'; @@ -659,6 +674,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String get deleteList => 'Supprimer la liste'; + @override + String get unshare => 'Ne plus partager'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Intégrée'; @@ -671,6 +697,16 @@ class AppLocalizationsFr extends AppLocalizations { return 'Supprimer « $title » de Google Tasks ?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Supprimer l’événement'; @@ -795,6 +831,74 @@ class AppLocalizationsFr extends AppLocalizations { @override String get doneStatus => 'Terminée'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Nécessite une action'; + + @override + String get taskStatusInProcess => 'En cours'; + + @override + String get taskStatusCompleted => 'Terminé'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priorité'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Notes'; @@ -831,6 +935,84 @@ class AppLocalizationsFr extends AppLocalizations { @override String get addReminder => 'Ajouter un rappel'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'Aucun rappel'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Avant le début de la tâche'; + + @override + String get beforeTaskDue => 'Avant l\'échéance de la tâche'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Ajouter un invité'; @@ -864,6 +1046,123 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeatYearly => 'Annuel'; + @override + String get repeatEvery => 'Répéter chaque'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Arrêter la répétition'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'le $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count fois'; + } + + @override + String repeatUntilSummary(String date) { + return 'jusqu\'au $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Importance'; @@ -913,6 +1212,26 @@ class AppLocalizationsFr extends AppLocalizations { @override String get createSubtask => 'Créer une sous-tâche'; + @override + String get subtasks => 'Sous-tâches'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Masquer les sous-tâches'; + + @override + String get hideClosedSubtasks => 'Masquer les sous-tâches fermées'; + @override String get moveToTop => 'Déplacer tout en haut'; @@ -924,7 +1243,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Supprimer « $title » de Google Tasks ?'; + return 'Supprimer « $title » ?'; } @override @@ -1252,6 +1571,223 @@ class AppLocalizationsFr extends AppLocalizations { @override String get noLocationsFound => 'Aucun lieu trouvé'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'L’échéance ne peut pas précéder le début.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Définissez une heure pour le début et l’échéance, ou passez la tâche en journée entière.'; + @override String deleteCalendarConfirmation(String title) { return 'Supprimer « $title » ?'; diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index 9ae2566..a7c2c19 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -17,7 +17,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get connectGoogleAccount => - 'कैलेंडर और कार्य सिंक करने के लिए Google और Microsoft खाते कनेक्ट करें।'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'वे सभी Google और Microsoft खाते जोड़ें जिन्हें आप उपयोग करना चाहते हैं। BusyMax प्रत्येक खाते के कैलेंडर, ईवेंट, कार्य सूचियाँ और कार्य सिंक करता है।'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'सिस्टम सेटिंग्स चुनें'; @@ -613,7 +613,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get removeAccountConfirmation => - 'इससे कैश किए गए कार्य, कैलेंडर, ईवेंट, रिमाइंडर और लंबित ऑफ़लाइन बदलाव इस डिवाइस से मिट जाएँगे। सिंक न किए गए बदलाव खो जाएँगे। Google या Microsoft से कुछ भी नहीं मिटेगा।'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -637,6 +637,21 @@ class AppLocalizationsHi extends AppLocalizations { @override String get newTaskList => 'नई कार्य सूची'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'कार्य सूचियाँ देखने के लिए साइन इन करें।'; @@ -659,6 +674,17 @@ class AppLocalizationsHi extends AppLocalizations { @override String get deleteList => 'सूची मिटाएँ'; + @override + String get unshare => 'Unshare'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'अंतर्निहित'; @@ -671,6 +697,16 @@ class AppLocalizationsHi extends AppLocalizations { return 'Google Tasks से “$title” मिटाएँ?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'ईवेंट मिटाएँ'; @@ -795,6 +831,74 @@ class AppLocalizationsHi extends AppLocalizations { @override String get doneStatus => 'पूर्ण'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'In process'; + + @override + String get taskStatusCompleted => 'Completed'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priority'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'नोट्स'; @@ -831,6 +935,84 @@ class AppLocalizationsHi extends AppLocalizations { @override String get addReminder => 'रिमाइंडर जोड़ें'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'अतिथि जोड़ें'; @@ -864,6 +1046,123 @@ class AppLocalizationsHi extends AppLocalizations { @override String get repeatYearly => 'हर वर्ष'; + @override + String get repeatEvery => 'Repeat every'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'End repeat'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count times'; + } + + @override + String repeatUntilSummary(String date) { + return 'until $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'महत्त्व'; @@ -913,6 +1212,26 @@ class AppLocalizationsHi extends AppLocalizations { @override String get createSubtask => 'उपकार्य बनाएँ'; + @override + String get subtasks => 'उपकार्य'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Hide subtasks'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => 'सबसे ऊपर ले जाएँ'; @@ -924,7 +1243,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Google Tasks से “$title” मिटाएँ?'; + return '“$title” मिटाएँ?'; } @override @@ -1243,6 +1562,223 @@ class AppLocalizationsHi extends AppLocalizations { @override String get noLocationsFound => 'कोई स्थान नहीं मिला'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'नियत समय प्रारंभ समय से पहले नहीं हो सकता।'; + + @override + String get taskStartDueTimeModeMismatch => + 'प्रारंभ और नियत समय दोनों सेट करें, या कार्य को पूरे दिन का बनाएँ।'; + @override String deleteCalendarConfirmation(String title) { return '“$title” मिटाएँ?'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 725f326..7918efe 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -17,7 +17,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get connectGoogleAccount => - 'Collega gli account Google e Microsoft per sincronizzare calendari e attività.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Aggiungi tutti gli account Google e Microsoft che vuoi utilizzare. BusyMax sincronizza calendari, eventi, elenchi di attività e attività di ogni account.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => @@ -615,7 +615,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get removeAccountConfirmation => - 'Questa azione elimina dal dispositivo attività, calendari, eventi e promemoria memorizzati nella cache, oltre alle modifiche offline in sospeso. Le modifiche non sincronizzate andranno perse. Non verrà eliminato nulla da Google o Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -639,6 +639,21 @@ class AppLocalizationsIt extends AppLocalizations { @override String get newTaskList => 'Nuovo elenco di attività'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Accedi per visualizzare gli elenchi di attività.'; @@ -662,6 +677,17 @@ class AppLocalizationsIt extends AppLocalizations { @override String get deleteList => 'Elimina elenco'; + @override + String get unshare => 'Rimuovi condivisione'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Integrato'; @@ -674,6 +700,16 @@ class AppLocalizationsIt extends AppLocalizations { return 'Eliminare «$title» da Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Elimina evento'; @@ -798,6 +834,74 @@ class AppLocalizationsIt extends AppLocalizations { @override String get doneStatus => 'Completata'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Richiede azione'; + + @override + String get taskStatusInProcess => 'In corso'; + + @override + String get taskStatusCompleted => 'Completato'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priorità'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Note'; @@ -834,6 +938,84 @@ class AppLocalizationsIt extends AppLocalizations { @override String get addReminder => 'Aggiungi promemoria'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'Nessun promemoria'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Prima dell\'inizio dell\'attività'; + + @override + String get beforeTaskDue => 'Prima della scadenza del compito'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Scarta i promemoria'; + + @override + String get keepRelatedReminders => 'Mantieni i promemoria'; + @override String get addGuest => 'Aggiungi invitato'; @@ -867,6 +1049,123 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeatYearly => 'Ogni anno'; + @override + String get repeatEvery => 'Ripeti ogni'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Termina ripetizione'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'il giorno $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count volte'; + } + + @override + String repeatUntilSummary(String date) { + return 'fino al $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Importanza'; @@ -916,6 +1215,26 @@ class AppLocalizationsIt extends AppLocalizations { @override String get createSubtask => 'Crea sottoattività'; + @override + String get subtasks => 'Sottoattività'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Nascondi sottoattività'; + + @override + String get hideClosedSubtasks => 'Nascondi sotto-attività chiuse'; + @override String get moveToTop => 'Sposta in cima'; @@ -927,7 +1246,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Eliminare «$title» da Google Tasks?'; + return 'Eliminare «$title»?'; } @override @@ -1255,6 +1574,223 @@ class AppLocalizationsIt extends AppLocalizations { @override String get noLocationsFound => 'Nessun luogo trovato'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'La scadenza non può precedere l\'inizio.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Imposta un orario sia per l\'inizio sia per la scadenza, oppure rendi l\'attività valida per l\'intera giornata.'; + @override String deleteCalendarConfirmation(String title) { return 'Eliminare «$title»?'; diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index 9ab1999..e97fc84 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -17,7 +17,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get connectGoogleAccount => - 'Google と Microsoft のアカウントを接続して、カレンダーとタスクを同期します。'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get onboardingAccountsStepDescription => - '使用するすべての Google アカウントと Microsoft アカウントを追加してください。BusyMax は各アカウントのカレンダー、予定、タスクリスト、タスクを同期します。'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'システム設定を選択'; @@ -600,7 +600,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get removeAccountConfirmation => - 'このデバイスにキャッシュされたタスク、カレンダー、予定、リマインダー、保留中のオフライン変更が削除されます。同期されていない変更は失われます。Google または Microsoft から削除されるデータはありません。'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => 'この Google アカウントへの BusyMax のアクセス権も取り消す'; @@ -621,6 +621,21 @@ class AppLocalizationsJa extends AppLocalizations { @override String get newTaskList => '新しいタスクリスト'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'タスクリストを表示するにはサインインしてください。'; @@ -642,6 +657,17 @@ class AppLocalizationsJa extends AppLocalizations { @override String get deleteList => 'リストを削除'; + @override + String get unshare => '共有を解除'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => '組み込み'; @@ -654,6 +680,16 @@ class AppLocalizationsJa extends AppLocalizations { return 'Google Tasks から「$title」を削除しますか?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => '予定を削除'; @@ -775,6 +811,74 @@ class AppLocalizationsJa extends AppLocalizations { @override String get doneStatus => '完了'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'アクションが必要'; + + @override + String get taskStatusInProcess => '進行中'; + + @override + String get taskStatusCompleted => '完了'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => '優先度'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'メモ'; @@ -811,6 +915,84 @@ class AppLocalizationsJa extends AppLocalizations { @override String get addReminder => 'リマインダーを追加'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'ゲストを追加'; @@ -844,6 +1026,123 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeatYearly => '毎年'; + @override + String get repeatEvery => '毎日繰り返す'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => '繰り返し終了'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count回'; + } + + @override + String repeatUntilSummary(String date) { + return '$dateまで'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => '重要度'; @@ -893,6 +1192,26 @@ class AppLocalizationsJa extends AppLocalizations { @override String get createSubtask => 'サブタスクを作成'; + @override + String get subtasks => 'サブタスク'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'サブタスクを非表示'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => '一番上に移動'; @@ -904,7 +1223,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Google Tasks から「$title」を削除しますか?'; + return '「$title」を削除しますか?'; } @override @@ -1219,6 +1538,222 @@ class AppLocalizationsJa extends AppLocalizations { @override String get noLocationsFound => '場所が見つかりません'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => '期限を開始より前に設定することはできません。'; + + @override + String get taskStartDueTimeModeMismatch => '開始と期限の両方に時刻を設定するか、タスクを終日にしてください。'; + @override String deleteCalendarConfirmation(String title) { return '「$title」を削除しますか?'; diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index adf823f..97ddc6f 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -17,7 +17,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get connectGoogleAccount => - 'Google 및 Microsoft 계정을 연결하여 캘린더와 할 일을 동기화하세요.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get onboardingAccountsStepDescription => - '사용할 Google 및 Microsoft 계정을 모두 추가하세요. BusyMax는 각 계정의 캘린더, 일정, 할 일 목록 및 할 일을 동기화합니다.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => '시스템 설정 선택'; @@ -600,7 +600,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get removeAccountConfirmation => - '이 기기에서 캐시된 할 일, 캘린더, 일정, 미리 알림 및 보류 중인 오프라인 변경 사항이 삭제됩니다. 동기화되지 않은 변경 사항은 사라집니다. Google 또는 Microsoft에서는 아무것도 삭제되지 않습니다.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => '이 Google 계정에 대한 BusyMax의 액세스 권한도 취소'; @@ -621,6 +621,21 @@ class AppLocalizationsKo extends AppLocalizations { @override String get newTaskList => '새 할 일 목록'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => '할 일 목록을 보려면 로그인하세요.'; @@ -642,6 +657,17 @@ class AppLocalizationsKo extends AppLocalizations { @override String get deleteList => '목록 삭제'; + @override + String get unshare => '공유 해제'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => '기본 제공'; @@ -654,6 +680,16 @@ class AppLocalizationsKo extends AppLocalizations { return 'Google Tasks에서 “$title” 목록을 삭제할까요?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => '일정 삭제'; @@ -775,6 +811,74 @@ class AppLocalizationsKo extends AppLocalizations { @override String get doneStatus => '완료'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'In process'; + + @override + String get taskStatusCompleted => '완료됨'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => '우선 순위'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => '메모'; @@ -811,6 +915,84 @@ class AppLocalizationsKo extends AppLocalizations { @override String get addReminder => '미리 알림 추가'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => '참석자 추가'; @@ -844,6 +1026,123 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeatYearly => '매년'; + @override + String get repeatEvery => '반복 주기'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => '반복 종료'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count회'; + } + + @override + String repeatUntilSummary(String date) { + return 'until $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => '중요도'; @@ -893,6 +1192,26 @@ class AppLocalizationsKo extends AppLocalizations { @override String get createSubtask => '하위 할 일 만들기'; + @override + String get subtasks => '하위 할 일'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Hide subtasks'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => '맨 위로 이동'; @@ -904,7 +1223,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Google Tasks에서 “$title” 항목을 삭제할까요?'; + return '“$title” 항목을 삭제할까요?'; } @override @@ -1219,6 +1538,223 @@ class AppLocalizationsKo extends AppLocalizations { @override String get noLocationsFound => '위치를 찾을 수 없습니다'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => '마감은 시작보다 빠를 수 없습니다.'; + + @override + String get taskStartDueTimeModeMismatch => + '시작과 마감에 모두 시간을 설정하거나 작업을 종일로 설정하세요.'; + @override String deleteCalendarConfirmation(String title) { return '“$title” 캘린더를 삭제할까요?'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 052f856..ab09256 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -17,7 +17,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get connectGoogleAccount => - 'Ligue contas Google e Microsoft para sincronizar calendários e tarefas.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Adicione todas as contas Google e Microsoft que pretende utilizar. O BusyMax sincroniza calendários, eventos, listas de tarefas e tarefas de cada conta.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Escolher definições do sistema'; @@ -615,7 +615,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get removeAccountConfirmation => - 'Esta ação elimina deste dispositivo as tarefas, os calendários, os eventos, os lembretes e as alterações offline pendentes em cache. As alterações não sincronizadas serão perdidas. Nada será eliminado da Google ou da Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -639,6 +639,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get newTaskList => 'Nova lista de tarefas'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Inicie sessão para ver as listas de tarefas.'; @@ -662,6 +677,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get deleteList => 'Eliminar lista'; + @override + String get unshare => 'Cancelar partilha'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Incorporada'; @@ -674,6 +700,16 @@ class AppLocalizationsPt extends AppLocalizations { return 'Eliminar «$title» do Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Eliminar evento'; @@ -798,6 +834,74 @@ class AppLocalizationsPt extends AppLocalizations { @override String get doneStatus => 'Concluída'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'Em andamento'; + + @override + String get taskStatusCompleted => 'Concluída'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Prioridade'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Notas'; @@ -834,6 +938,84 @@ class AppLocalizationsPt extends AppLocalizations { @override String get addReminder => 'Adicionar lembrete'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Adicionar convidado'; @@ -867,6 +1049,123 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeatYearly => 'Anualmente'; + @override + String get repeatEvery => 'Repetir a cada'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'End repeat'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'em $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count vezes'; + } + + @override + String repeatUntilSummary(String date) { + return 'até $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Importância'; @@ -916,6 +1215,26 @@ class AppLocalizationsPt extends AppLocalizations { @override String get createSubtask => 'Criar subtarefa'; + @override + String get subtasks => 'Subtarefas'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Ocultar subtarefas'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => 'Mover para o início'; @@ -927,7 +1246,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Eliminar «$title» do Google Tasks?'; + return 'Eliminar «$title»?'; } @override @@ -1253,6 +1572,223 @@ class AppLocalizationsPt extends AppLocalizations { @override String get noLocationsFound => 'Nenhum local encontrado'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'O prazo não pode ser anterior ao início.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Defina horários para o início e o prazo, ou torne a tarefa de dia inteiro.'; + @override String deleteCalendarConfirmation(String title) { return 'Eliminar «$title»?'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 7db883b..371e8b2 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -17,7 +17,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get connectGoogleAccount => - 'Подключите аккаунты Google и Microsoft, чтобы синхронизировать календари и задачи.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Добавьте все аккаунты Google и Microsoft, которые хотите использовать. BusyMax синхронизирует календари, события, списки задач и задачи из каждого аккаунта.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Выберите системные параметры'; @@ -618,7 +618,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get removeAccountConfirmation => - 'С этого устройства будут удалены кэшированные задачи, календари, события, напоминания и локальные изменения, ожидающие синхронизации. Несинхронизированные изменения будут потеряны. В Google и Microsoft ничего не будет удалено.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -642,6 +642,21 @@ class AppLocalizationsRu extends AppLocalizations { @override String get newTaskList => 'Новый список задач'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Войдите, чтобы просмотреть списки задач.'; @@ -664,6 +679,17 @@ class AppLocalizationsRu extends AppLocalizations { @override String get deleteList => 'Удалить список'; + @override + String get unshare => 'Закрыть доступ'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Встроенный'; @@ -676,6 +702,16 @@ class AppLocalizationsRu extends AppLocalizations { return 'Удалить «$title» из Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Удалить событие'; @@ -800,6 +836,74 @@ class AppLocalizationsRu extends AppLocalizations { @override String get doneStatus => 'Выполнена'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Требуется действие'; + + @override + String get taskStatusInProcess => 'Выполянется'; + + @override + String get taskStatusCompleted => 'Завершённые'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Приоритет'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Заметки'; @@ -836,6 +940,84 @@ class AppLocalizationsRu extends AppLocalizations { @override String get addReminder => 'Добавить напоминание'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'Напоминаний нет'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'До начала задачи'; + + @override + String get beforeTaskDue => 'До срока выполнения задачи'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Отменить напоминания'; + + @override + String get keepRelatedReminders => 'Сохранить напоминания'; + @override String get addGuest => 'Добавить гостя'; @@ -869,6 +1051,123 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeatYearly => 'Ежегодно'; + @override + String get repeatEvery => 'Повторять каждые'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Прекратить повтор'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'в $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return ' $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'в $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count раз'; + } + + @override + String repeatUntilSummary(String date) { + return 'до $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Важность'; @@ -918,6 +1217,26 @@ class AppLocalizationsRu extends AppLocalizations { @override String get createSubtask => 'Создать подзадачу'; + @override + String get subtasks => 'Подзадачи'; + + @override + String get duplicateTask => 'Дублировать задачу'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Скрыть вложенные задачи'; + + @override + String get hideClosedSubtasks => 'Скрыть закрытые подзадачи'; + @override String get moveToTop => 'Переместить в самый верх'; @@ -929,7 +1248,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Удалить «$title» из Google Tasks?'; + return 'Удалить «$title»?'; } @override @@ -1256,6 +1575,223 @@ class AppLocalizationsRu extends AppLocalizations { @override String get noLocationsFound => 'Ничего не найдено'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => 'Срок не может быть раньше начала.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Укажите время начала и срока или сделайте задачу на весь день.'; + @override String deleteCalendarConfirmation(String title) { return 'Удалить «$title»?'; diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index 4a14bf5..dc2e534 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -17,7 +17,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get connectGoogleAccount => - 'Kết nối tài khoản Google và Microsoft để đồng bộ lịch và công việc.'; + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => @@ -41,7 +41,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get onboardingAccountsStepDescription => - 'Thêm tất cả tài khoản Google và Microsoft bạn muốn sử dụng. BusyMax đồng bộ lịch, sự kiện, danh sách công việc và công việc từ mỗi tài khoản.'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => 'Chọn cài đặt hệ thống'; @@ -612,7 +612,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String get removeAccountConfirmation => - 'Thao tác này sẽ xóa công việc, lịch, sự kiện, lời nhắc đã lưu trong bộ nhớ đệm và các thay đổi ngoại tuyến đang chờ khỏi thiết bị. Các thay đổi chưa đồng bộ sẽ bị mất. Không có dữ liệu nào bị xóa khỏi Google hoặc Microsoft.'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => @@ -636,6 +636,21 @@ class AppLocalizationsVi extends AppLocalizations { @override String get newTaskList => 'Danh sách công việc mới'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => 'Đăng nhập để xem danh sách công việc.'; @@ -658,6 +673,17 @@ class AppLocalizationsVi extends AppLocalizations { @override String get deleteList => 'Xóa danh sách'; + @override + String get unshare => 'Bỏ chia sẽ'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => 'Tích hợp sẵn'; @@ -670,6 +696,16 @@ class AppLocalizationsVi extends AppLocalizations { return 'Xóa “$title” khỏi Google Tasks?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => 'Xóa sự kiện'; @@ -794,6 +830,74 @@ class AppLocalizationsVi extends AppLocalizations { @override String get doneStatus => 'Đã hoàn thành'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => 'Needs action'; + + @override + String get taskStatusInProcess => 'In process'; + + @override + String get taskStatusCompleted => 'Hoàn thành'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => 'Priority'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => 'Ghi chú'; @@ -830,6 +934,84 @@ class AppLocalizationsVi extends AppLocalizations { @override String get addReminder => 'Thêm lời nhắc'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => 'No reminders'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => 'Before the task starts'; + + @override + String get beforeTaskDue => 'Before the task is due'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => 'Discard reminders'; + + @override + String get keepRelatedReminders => 'Keep reminders'; + @override String get addGuest => 'Thêm khách mời'; @@ -863,6 +1045,123 @@ class AppLocalizationsVi extends AppLocalizations { @override String get repeatYearly => 'Hằng năm'; + @override + String get repeatEvery => 'Lặp lại mỗi'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => 'Kết thúc lập lại'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return 'on $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return 'on day $days'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return 'in $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count lần'; + } + + @override + String repeatUntilSummary(String date) { + return 'cho đến $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => 'Mức độ quan trọng'; @@ -912,6 +1211,26 @@ class AppLocalizationsVi extends AppLocalizations { @override String get createSubtask => 'Tạo công việc con'; + @override + String get subtasks => 'Công việc con'; + + @override + String get duplicateTask => 'Duplicate task'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => 'Hide subtasks'; + + @override + String get hideClosedSubtasks => 'Hide closed subtasks'; + @override String get moveToTop => 'Chuyển lên đầu'; @@ -923,7 +1242,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return 'Xóa “$title” khỏi Google Tasks?'; + return 'Xóa “$title”?'; } @override @@ -1244,6 +1563,224 @@ class AppLocalizationsVi extends AppLocalizations { @override String get noLocationsFound => 'Không tìm thấy địa điểm'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => + 'Hạn chót không được trước thời gian bắt đầu.'; + + @override + String get taskStartDueTimeModeMismatch => + 'Đặt giờ cho cả thời gian bắt đầu và hạn chót, hoặc đặt công việc là cả ngày.'; + @override String deleteCalendarConfirmation(String title) { return 'Xóa “$title”?'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 0e6292d..29d54a0 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -16,7 +16,8 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'BusyMax'; @override - String get connectGoogleAccount => '连接 Google 和 Microsoft 帐户以同步日历和任务。'; + String get connectGoogleAccount => + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。'; @@ -39,7 +40,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get onboardingAccountsStepDescription => - '添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => '选择系统设置'; @@ -592,7 +593,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get removeAccountConfirmation => - '这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => '同时撤销 BusyMax 对此 Google 帐户的访问权限'; @@ -613,6 +614,21 @@ class AppLocalizationsZh extends AppLocalizations { @override String get newTaskList => '新建任务列表'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => '登录以查看任务列表。'; @@ -634,6 +650,17 @@ class AppLocalizationsZh extends AppLocalizations { @override String get deleteList => '删除列表'; + @override + String get unshare => '取消共享'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => '内置'; @@ -646,6 +673,16 @@ class AppLocalizationsZh extends AppLocalizations { return '从 Google Tasks 中删除“$title”?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => '删除日程'; @@ -767,6 +804,74 @@ class AppLocalizationsZh extends AppLocalizations { @override String get doneStatus => '已完成'; + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => '需要操作'; + + @override + String get taskStatusInProcess => '处理中'; + + @override + String get taskStatusCompleted => '完成'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => '优先级'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + @override String get notes => '备注'; @@ -803,6 +908,84 @@ class AppLocalizationsZh extends AppLocalizations { @override String get addReminder => '添加提醒'; + @override + String get reminders => 'Reminders'; + + @override + String get noReminders => '无提醒'; + + @override + String get editReminder => 'Edit reminder'; + + @override + String get beforeTaskStarts => '任务开始前'; + + @override + String get beforeTaskDue => '任务截止前'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => '舍弃提醒'; + + @override + String get keepRelatedReminders => '保留提醒'; + @override String get addGuest => '添加参与者'; @@ -836,6 +1019,123 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeatYearly => '每年'; + @override + String get repeatEvery => '重复每'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => '结束重复'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return '在 $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return '在第 $days 天'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return '在 $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count次'; + } + + @override + String repeatUntilSummary(String date) { + return '至 $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + @override String get importance => '重要性'; @@ -884,6 +1184,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String get createSubtask => '创建子任务'; + @override + String get subtasks => '子任务'; + + @override + String get duplicateTask => '复制任务'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => '隐藏子任务'; + + @override + String get hideClosedSubtasks => '隐藏关闭的子任务'; + @override String get moveToTop => '移到顶部'; @@ -895,7 +1215,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String deleteTaskConfirmation(String title) { - return '从 Google Tasks 中删除“$title”?'; + return '删除“$title”?'; } @override @@ -1211,141 +1531,358 @@ class AppLocalizationsZh extends AppLocalizations { String get noLocationsFound => '未找到地点'; @override - String deleteCalendarConfirmation(String title) { - return '删除“$title”?'; - } -} + String get requiredField => 'This field is required.'; -/// The translations for Chinese, using the Han script (`zh_Hans`). -class AppLocalizationsZhHans extends AppLocalizationsZh { - AppLocalizationsZhHans() : super('zh_Hans'); + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; @override - String get appTitle => 'BusyMax'; + String get appleICloudProvider => 'Apple iCloud Calendar'; @override - String get connectGoogleAccount => '连接 Google 和 Microsoft 帐户以同步日历和任务。'; + String get nextcloudProvider => 'Nextcloud'; @override - String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。'; + String get appleICloudTasksProvider => 'Apple iCloud'; @override - String get googlePermissionsRequiredRetry => - '必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。'; + String get nextcloudTasksProvider => 'Nextcloud Tasks'; @override - String get finishSetup => '完成设置'; + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; @override - String get continueSetup => '继续'; + String get addNextcloudAccount => 'Add Nextcloud account'; @override - String get onboardingSetupTitle => '设置 BusyMax'; + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; @override - String get onboardingAccountsStepTitle => '连接帐户'; + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; @override - String get onboardingAccountsStepDescription => - '添加您要使用的所有 Google 和 Microsoft 帐户。BusyMax 会同步每个帐户中的日历、日程、任务列表和任务。'; + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; @override - String get onboardingPreferencesStepTitle => '选择系统设置'; + String get appleAccountEmail => 'Apple Account email'; @override - String get onboardingPreferencesStepDescription => - '打开日程前,请设置桌面行为、提醒、通知详细程度和外观。'; + String get appleAppSpecificPassword => 'App-specific password'; @override - String get signInWithGoogle => '使用 Google 登录'; + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; @override - String get signInWithMicrosoft => '使用 Microsoft 登录'; + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; @override - String get googleTasksProvider => 'Google Tasks'; + String get connectNextcloudTitle => 'Connect Nextcloud'; @override - String get microsoftTodoProvider => 'Microsoft To Do'; + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; @override - String get providerNotConfigured => '尚未配置此服务。'; + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; @override - String get waitingForGoogleSignIn => '正在等待 Google 登录...'; + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; @override - String get waitingForMicrosoftSignIn => '正在等待 Microsoft 登录...'; + String get connectAccountAction => 'Connect'; @override - String get microsoftSignInNotConfigured => - '尚未配置 Microsoft 登录。请设置 MICROSOFT_OAUTH_CLIENT_ID。'; + String get cancelAccountConnection => 'Cancel connection'; @override - String get cancel => '取消'; + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; @override - String get close => '关闭'; + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; @override - String get exit => '退出'; + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; @override - String get options => '选项'; + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; @override - String get hide => '隐藏'; + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; @override - String get show => '显示'; + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; @override - String get export => '导出'; + String get collectionSettings => 'Collections'; @override - String get save => '保存'; + String get calendarContent => 'Calendar events'; @override - String get settings => '设置'; + String get taskContent => 'Tasks'; @override - String get all => '全部'; + String get readOnlySharedCollection => 'Read-only or shared'; @override - String get calendarEvents => '日程'; + String get pendingLocally => 'Pending locally'; @override - String get calendarTasks => '任务'; + String get conflictBlocked => 'Blocked by conflict'; @override - String get calendar => '日历'; + String get authenticationBlocked => 'Blocked until reconnect'; @override - String get calendars => '日历'; + String get operationFailed => 'Operation failed'; @override - String get newEvent => '新建日程'; + String get keepServerVersion => 'Keep server version'; @override - String get refreshCalendar => '刷新日历'; + String get reapplyLocalChange => 'Review and reapply local change'; @override - String get openInProvider => '在服务中打开'; + String get duplicateLocalItem => 'Duplicate as new item'; @override - String get hideFromSchedule => '从日程中隐藏'; + String get davConnectionState => 'Connection state'; @override - String get showInSchedule => '在日程中显示'; + String get davConnected => 'Connected'; @override - String get noCalendarsSynced => '尚未同步任何日历。'; + String get davConnecting => 'Connecting…'; @override - String get allDay => '全天'; + String get davSignedOut => 'Signed out'; @override - String moreItems(int count) { + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => '截止时间不能早于开始时间。'; + + @override + String get taskStartDueTimeModeMismatch => '请同时设置开始和截止时间,或将任务设为全天。'; + + @override + String deleteCalendarConfirmation(String title) { + return '删除“$title”?'; + } +} + +/// The translations for Chinese, using the Han script (`zh_Hans`). +class AppLocalizationsZhHans extends AppLocalizationsZh { + AppLocalizationsZhHans() : super('zh_Hans'); + + @override + String get appTitle => 'BusyMax'; + + @override + String get connectGoogleAccount => + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; + + @override + String get googlePermissionsConsentNotice => '在 Google 权限页面上,同时选择日历和任务权限。'; + + @override + String get googlePermissionsRequiredRetry => + '必须授予 Google 日历和 Google Tasks 权限。请重试并选中两个复选框。'; + + @override + String get finishSetup => '完成设置'; + + @override + String get continueSetup => '继续'; + + @override + String get onboardingSetupTitle => '设置 BusyMax'; + + @override + String get onboardingAccountsStepTitle => '连接帐户'; + + @override + String get onboardingAccountsStepDescription => + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; + + @override + String get onboardingPreferencesStepTitle => '选择系统设置'; + + @override + String get onboardingPreferencesStepDescription => + '打开日程前,请设置桌面行为、提醒、通知详细程度和外观。'; + + @override + String get signInWithGoogle => '使用 Google 登录'; + + @override + String get signInWithMicrosoft => '使用 Microsoft 登录'; + + @override + String get googleTasksProvider => 'Google Tasks'; + + @override + String get microsoftTodoProvider => 'Microsoft To Do'; + + @override + String get providerNotConfigured => '尚未配置此服务。'; + + @override + String get waitingForGoogleSignIn => '正在等待 Google 登录...'; + + @override + String get waitingForMicrosoftSignIn => '正在等待 Microsoft 登录...'; + + @override + String get microsoftSignInNotConfigured => + '尚未配置 Microsoft 登录。请设置 MICROSOFT_OAUTH_CLIENT_ID。'; + + @override + String get cancel => '取消'; + + @override + String get close => '关闭'; + + @override + String get exit => '退出'; + + @override + String get options => '选项'; + + @override + String get hide => '隐藏'; + + @override + String get show => '显示'; + + @override + String get export => '导出'; + + @override + String get save => '保存'; + + @override + String get settings => '设置'; + + @override + String get all => '全部'; + + @override + String get calendarEvents => '日程'; + + @override + String get calendarTasks => '任务'; + + @override + String get calendar => '日历'; + + @override + String get calendars => '日历'; + + @override + String get newEvent => '新建日程'; + + @override + String get refreshCalendar => '刷新日历'; + + @override + String get openInProvider => '在服务中打开'; + + @override + String get hideFromSchedule => '从日程中隐藏'; + + @override + String get showInSchedule => '在日程中显示'; + + @override + String get noCalendarsSynced => '尚未同步任何日历。'; + + @override + String get allDay => '全天'; + + @override + String moreItems(int count) { return '还有 $count 项'; } @@ -1800,7 +2337,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @override String get removeAccountConfirmation => - '这会从此设备删除缓存的任务、日历、日程、提醒和待处理的离线更改。未同步的更改将丢失。不会从 Google 或 Microsoft 删除任何内容。'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => '同时撤销 BusyMax 对此 Google 帐户的访问权限'; @@ -1821,6 +2358,21 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @override String get newTaskList => '新建任务列表'; + @override + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + @override String get signInToViewTaskLists => '登录以查看任务列表。'; @@ -1842,6 +2394,17 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @override String get deleteList => '删除列表'; + @override + String get unshare => '取消共享'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + @override String get builtInMicrosoftList => '内置'; @@ -1854,6 +2417,16 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { return '从 Google Tasks 中删除“$title”?'; } + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + @override String get deleteEvent => '删除日程'; @@ -1976,112 +2549,375 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { String get doneStatus => '已完成'; @override - String get notes => '备注'; + String get taskStatus => 'Status'; @override - String get dueDate => '截止日期'; + String get taskStatusNone => 'No status'; @override - String get clearDueDate => '清除截止日期'; + String get taskStatusNeedsAction => '需要操作'; @override - String get dueTime => '截止时间'; + String get taskStatusInProcess => '处理中'; @override - String get startDate => '开始日期'; + String get taskStatusCompleted => '完成'; @override - String get startTime => '开始时间'; + String get taskStatusCancelled => 'Cancelled'; @override - String get endDate => '结束日期'; + String completionPercent(int percent) { + return '$percent% completed'; + } @override - String get endTime => '结束时间'; + String get completionDate => 'Completion date'; @override - String get reminderDate => '提醒日期'; + String get priority => '优先级'; @override - String get reminderTime => '提醒时间'; + String get priorityNone => 'No priority'; @override - String get reminder => '提醒'; + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } @override - String get addReminder => '添加提醒'; + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } @override - String get addGuest => '添加参与者'; + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } @override - String get addGuestEmail => '添加参与者电子邮件'; + String get taskUrl => 'URL'; @override - String get removeReminder => '移除提醒'; + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; @override - String get off => '关闭'; + String get classification => 'Classification'; @override - String get repeat => '重复'; + String get classificationPublic => 'When shared, show the full task'; @override - String get repeatNone => '不重复'; + String get classificationConfidential => 'When shared, show only busy'; @override - String get noneValue => '无'; + String get classificationPrivate => 'When shared, hide this task'; @override - String get repeatDaily => '每天'; + String get pinTask => 'Pin task'; @override - String get repeatWeekly => '每周'; + String get notes => '备注'; @override - String get repeatMonthly => '每月'; + String get dueDate => '截止日期'; @override - String get repeatYearly => '每年'; + String get clearDueDate => '清除截止日期'; @override - String get importance => '重要性'; + String get dueTime => '截止时间'; @override - String get importanceLow => '低'; + String get startDate => '开始日期'; @override - String get importanceNormal => '普通'; + String get startTime => '开始时间'; @override - String get importanceHigh => '高'; + String get endDate => '结束日期'; @override - String get categories => '类别'; + String get endTime => '结束时间'; @override - String get scheduleSection => '日程'; + String get reminderDate => '提醒日期'; @override - String get dueGroup => '截止'; + String get reminderTime => '提醒时间'; @override - String get startGroup => '开始'; + String get reminder => '提醒'; @override - String get reminderGroup => '提醒'; + String get addReminder => '添加提醒'; @override - String get organizationSection => '整理'; + String get reminders => 'Reminders'; @override - String get actionsSection => '操作'; + String get noReminders => '无提醒'; @override - String get advancedSection => '高级'; + String get editReminder => 'Edit reminder'; @override - String get addCategory => '添加类别'; + String get beforeTaskStarts => '任务开始前'; + + @override + String get beforeTaskDue => '任务截止前'; + + @override + String get afterTaskStarts => 'After the task starts'; + + @override + String get afterTaskDue => 'After the task is due'; + + @override + String get relativeToTaskStart => 'Relative to the task start date'; + + @override + String get relativeToTaskDue => 'Relative to the task due date'; + + @override + String get reminderTimeOfDay => 'Time of day'; + + @override + String get absoluteReminder => 'At a date and time'; + + @override + String get reminderAmount => 'Amount'; + + @override + String get reminderUnit => 'Unit'; + + @override + String get reminderUnitSeconds => 'Seconds'; + + @override + String get reminderUnitMinutes => 'Minutes'; + + @override + String get reminderUnitHours => 'Hours'; + + @override + String get reminderUnitDays => 'Days'; + + @override + String get reminderUnitWeeks => 'Weeks'; + + @override + String get reminderAtTaskStart => 'At the task start'; + + @override + String get reminderAtTaskDue => 'At the task due time'; + + @override + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; + + @override + String get relatedRemindersTitle => 'Keep related reminders?'; + + @override + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } + + @override + String get discardRelatedReminders => '舍弃提醒'; + + @override + String get keepRelatedReminders => '保留提醒'; + + @override + String get addGuest => '添加参与者'; + + @override + String get addGuestEmail => '添加参与者电子邮件'; + + @override + String get removeReminder => '移除提醒'; + + @override + String get off => '关闭'; + + @override + String get repeat => '重复'; + + @override + String get repeatNone => '不重复'; + + @override + String get noneValue => '无'; + + @override + String get repeatDaily => '每天'; + + @override + String get repeatWeekly => '每周'; + + @override + String get repeatMonthly => '每月'; + + @override + String get repeatYearly => '每年'; + + @override + String get repeatEvery => '重复每'; + + @override + String get repeatOn => 'Repeat on'; + + @override + String get repeatEnd => '结束重复'; + + @override + String get repeatNever => 'Never'; + + @override + String get repeatUntil => 'On date'; + + @override + String get repeatAfter => 'After a number of occurrences'; + + @override + String get repeatCount => 'Occurrences'; + + @override + String get repeatDayOfMonth => 'Days of month'; + + @override + String get repeatMonths => 'Months'; + + @override + String get repeatOrdinal => 'Weekday position'; + + @override + String get repeatSpecificDays => 'Specific days'; + + @override + String get repeatFirst => 'First'; + + @override + String get repeatSecond => 'Second'; + + @override + String get repeatThird => 'Third'; + + @override + String get repeatFourth => 'Fourth'; + + @override + String get repeatFifth => 'Fifth'; + + @override + String get repeatSecondToLast => 'Second to last'; + + @override + String get repeatLast => 'Last'; + + @override + String get repeatAnyDay => 'Day'; + + @override + String get repeatWeekday => 'Weekday'; + + @override + String get repeatWeekendDay => 'Weekend day'; + + @override + String repeatEveryDays(int count) { + return 'Every $count days'; + } + + @override + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } + + @override + String repeatEveryMonths(int count) { + return 'Every $count months'; + } + + @override + String repeatEveryYears(int count) { + return 'Every $count years'; + } + + @override + String repeatOnDaysSummary(String days) { + return '在 $days'; + } + + @override + String repeatOnMonthDaysSummary(String days) { + return '在第 $days 天'; + } + + @override + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } + + @override + String repeatInMonthsSummary(String months) { + return '在 $months'; + } + + @override + String repeatTimesSummary(int count) { + return '$count次'; + } + + @override + String repeatUntilSummary(String date) { + return '至 $date'; + } + + @override + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; + + @override + String get importance => '重要性'; + + @override + String get importanceLow => '低'; + + @override + String get importanceNormal => '普通'; + + @override + String get importanceHigh => '高'; + + @override + String get categories => '类别'; + + @override + String get scheduleSection => '日程'; + + @override + String get dueGroup => '截止'; + + @override + String get startGroup => '开始'; + + @override + String get reminderGroup => '提醒'; + + @override + String get organizationSection => '整理'; + + @override + String get actionsSection => '操作'; + + @override + String get advancedSection => '高级'; + + @override + String get addCategory => '添加类别'; @override String get list => '列表'; @@ -2092,6 +2928,26 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @override String get createSubtask => '创建子任务'; + @override + String get subtasks => '子任务'; + + @override + String get duplicateTask => '复制任务'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => '隐藏子任务'; + + @override + String get hideClosedSubtasks => '隐藏关闭的子任务'; + @override String get moveToTop => '移到顶部'; @@ -2103,7 +2959,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @override String deleteTaskConfirmation(String title) { - return '从 Google Tasks 中删除“$title”?'; + return '删除“$title”?'; } @override @@ -2407,16 +3263,232 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { } @override - String get readOnlyCalendar => '此日历为只读。'; + String get readOnlyCalendar => '此日历为只读。'; + + @override + String get selectTimeZone => '选择时区'; + + @override + String get searchLocations => '搜索地点'; + + @override + String get noLocationsFound => '未找到地点'; + + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; @override - String get selectTimeZone => '选择时区'; + String get thisAndFutureUnavailable => 'This and future (not available)'; @override - String get searchLocations => '搜索地点'; + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; @override - String get noLocationsFound => '未找到地点'; + String get taskDueBeforeStart => '截止时间不能早于开始时间。'; + + @override + String get taskStartDueTimeModeMismatch => '请同时设置开始和截止时间,或将任务设为全天。'; @override String deleteCalendarConfirmation(String title) { @@ -2432,7 +3504,8 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { String get appTitle => 'BusyMax'; @override - String get connectGoogleAccount => '連結 Google 和 Microsoft 帳戶以同步行事曆和待辦事項。'; + String get connectGoogleAccount => + 'Connect Google, Microsoft, Apple iCloud Calendar, or Nextcloud accounts.'; @override String get googlePermissionsConsentNotice => '在 Google 權限畫面中,同時選取行事曆和待辦事項權限。'; @@ -2455,7 +3528,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get onboardingAccountsStepDescription => - '新增您要使用的所有 Google 和 Microsoft 帳戶。BusyMax 會同步每個帳戶中的行事曆、活動、待辦清單和待辦事項。'; + 'Add every account you want to use. BusyMax syncs supported calendars, events, task lists, and tasks from each account.'; @override String get onboardingPreferencesStepTitle => '選擇系統設定'; @@ -3008,7 +4081,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get removeAccountConfirmation => - '這會從此裝置刪除快取的待辦事項、行事曆、活動、提醒和待處理的離線變更。未同步的變更將會遺失。不會從 Google 或 Microsoft 刪除任何內容。'; + 'This deletes cached tasks, calendars, events, reminders, and pending offline changes from this device. Unsynced changes will be lost. Provider copies of calendars, events, task lists, and tasks are not deleted.'; @override String get revokeGoogleAccess => '同時撤銷 BusyMax 對此 Google 帳戶的存取權'; @@ -3030,227 +4103,526 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { String get newTaskList => '新增待辦清單'; @override - String get signInToViewTaskLists => '登入以查看待辦清單。'; + String taskListCreateFailed(String error) { + return 'Could not create the task list: $error'; + } + + @override + String taskListRenameFailed(String error) { + return 'Could not rename the task list: $error'; + } + + @override + String taskListDeleteFailed(String error) { + return 'Could not delete the task list: $error'; + } + + @override + String get signInToViewTaskLists => '登入以查看待辦清單。'; + + @override + String get noTaskListsSynced => '尚未同步任何待辦清單。'; + + @override + String get listActions => '清單動作'; + + @override + String get rename => '重新命名'; + + @override + String get delete => '刪除'; + + @override + String get renameList => '重新命名清單'; + + @override + String get deleteList => '刪除清單'; + + @override + String get unshare => '撤回分享'; + + @override + String get readOnlyTaskListCannotRename => + 'This task list is read-only and cannot be renamed.'; + + @override + String get taskListCannotDelete => + 'This task list cannot be deleted with your current permissions.'; + + @override + String get builtInMicrosoftList => '內建'; + + @override + String get builtInMicrosoftListCannotRenameDelete => + '無法重新命名或刪除 Microsoft To Do 內建清單。'; + + @override + String deleteListConfirmation(String title) { + return '要從 Google Tasks 刪除「$title」嗎?'; + } + + @override + String deleteTaskListConfirmation(String title) { + return 'Delete \"$title\" and all of its tasks?'; + } + + @override + String unshareTaskListConfirmation(String title) { + return 'Unshare \"$title\" from this account?'; + } + + @override + String get deleteEvent => '刪除活動'; + + @override + String get title => '標題'; + + @override + String get create => '新增'; + + @override + String get newTask => '新增待辦事項'; + + @override + String get clearCompleted => '清除已完成項目'; + + @override + String get refreshList => '重新整理清單'; + + @override + String get refreshAll => '全部重新整理'; + + @override + String get listRefreshed => '清單已重新整理。'; + + @override + String get allTasksRefreshed => '所有帳戶都已重新整理。'; + + @override + String exportedFile(String path) { + return '已匯出至 $path'; + } + + @override + String exportFailed(String error) { + return '匯出失敗:$error'; + } + + @override + String refreshFailed(String error) { + return '重新整理失敗:$error'; + } + + @override + String get selectOrCreateTaskList => '請選擇或建立待辦清單以開始使用。'; + + @override + String get signInToViewTasks => '登入以查看待辦事項。'; + + @override + String get noTasks => '沒有待辦事項。'; + + @override + String get noTasksYet => '還沒有待辦事項'; + + @override + String get noTasksYetMessage => '建立待辦事項或重新整理帳戶以開始使用。'; + + @override + String get noTasksInList => '此清單中沒有待辦事項。'; + + @override + String get overdue => '已逾期'; + + @override + String get today => '今天'; + + @override + String get tomorrow => '明天'; + + @override + String get upcoming => '即將到期'; + + @override + String get noDate => '無日期'; + + @override + String get completed => '已完成'; + + @override + String duePrefix(String date) { + return '$date 到期'; + } + + @override + String dateTimeDisplay(String date, String time) { + return '$date · $time'; + } + + @override + String get taskDetails => '待辦事項詳細資料'; + + @override + String get editTask => '編輯待辦事項'; + + @override + String get noTaskSelected => '未選取待辦事項。'; + + @override + String get noTaskSelectedHelper => '選擇待辦事項以查看和編輯詳細資料。'; + + @override + String get taskUnavailable => '無法使用待辦事項。'; + + @override + String get signInToEditTasks => '登入以編輯待辦事項。'; + + @override + String get refreshTask => '重新整理待辦事項'; + + @override + String get primarySection => '主要資訊'; + + @override + String get statusSection => '狀態'; + + @override + String get openStatus => '未完成'; + + @override + String get doneStatus => '已完成'; + + @override + String get taskStatus => 'Status'; + + @override + String get taskStatusNone => 'No status'; + + @override + String get taskStatusNeedsAction => '需要動作'; + + @override + String get taskStatusInProcess => '進行中'; + + @override + String get taskStatusCompleted => '完成'; + + @override + String get taskStatusCancelled => 'Cancelled'; + + @override + String completionPercent(int percent) { + return '$percent% completed'; + } + + @override + String get completionDate => 'Completion date'; + + @override + String get priority => '優先'; + + @override + String get priorityNone => 'No priority'; + + @override + String priorityHighValue(int priority) { + return 'Priority $priority · High'; + } + + @override + String priorityMediumValue(int priority) { + return 'Priority $priority · Medium'; + } + + @override + String priorityLowValue(int priority) { + return 'Priority $priority · Low'; + } + + @override + String get taskUrl => 'URL'; + + @override + String get invalidTaskUrl => 'Enter an absolute URL, including its scheme.'; + + @override + String get classification => 'Classification'; + + @override + String get classificationPublic => 'When shared, show the full task'; + + @override + String get classificationConfidential => 'When shared, show only busy'; + + @override + String get classificationPrivate => 'When shared, hide this task'; + + @override + String get pinTask => 'Pin task'; + + @override + String get notes => '備註'; + + @override + String get dueDate => '到期日'; + + @override + String get clearDueDate => '清除到期日'; + + @override + String get dueTime => '到期時間'; + + @override + String get startDate => '開始日期'; + + @override + String get startTime => '開始時間'; + + @override + String get endDate => '結束日期'; + + @override + String get endTime => '結束時間'; + + @override + String get reminderDate => '提醒日期'; + + @override + String get reminderTime => '提醒時間'; + + @override + String get reminder => '提醒'; @override - String get noTaskListsSynced => '尚未同步任何待辦清單。'; + String get addReminder => '新增提醒'; @override - String get listActions => '清單動作'; + String get reminders => 'Reminders'; @override - String get rename => '重新命名'; + String get noReminders => '無提醒'; @override - String get delete => '刪除'; + String get editReminder => 'Edit reminder'; @override - String get renameList => '重新命名清單'; + String get beforeTaskStarts => '在任務開始前'; @override - String get deleteList => '刪除清單'; + String get beforeTaskDue => '任務截止前'; @override - String get builtInMicrosoftList => '內建'; + String get afterTaskStarts => 'After the task starts'; @override - String get builtInMicrosoftListCannotRenameDelete => - '無法重新命名或刪除 Microsoft To Do 內建清單。'; + String get afterTaskDue => 'After the task is due'; @override - String deleteListConfirmation(String title) { - return '要從 Google Tasks 刪除「$title」嗎?'; - } + String get relativeToTaskStart => 'Relative to the task start date'; @override - String get deleteEvent => '刪除活動'; + String get relativeToTaskDue => 'Relative to the task due date'; @override - String get title => '標題'; + String get reminderTimeOfDay => 'Time of day'; @override - String get create => '新增'; + String get absoluteReminder => 'At a date and time'; @override - String get newTask => '新增待辦事項'; + String get reminderAmount => 'Amount'; @override - String get clearCompleted => '清除已完成項目'; + String get reminderUnit => 'Unit'; @override - String get refreshList => '重新整理清單'; + String get reminderUnitSeconds => 'Seconds'; @override - String get refreshAll => '全部重新整理'; + String get reminderUnitMinutes => 'Minutes'; @override - String get listRefreshed => '清單已重新整理。'; + String get reminderUnitHours => 'Hours'; @override - String get allTasksRefreshed => '所有帳戶都已重新整理。'; + String get reminderUnitDays => 'Days'; @override - String exportedFile(String path) { - return '已匯出至 $path'; - } + String get reminderUnitWeeks => 'Weeks'; @override - String exportFailed(String error) { - return '匯出失敗:$error'; - } + String get reminderAtTaskStart => 'At the task start'; @override - String refreshFailed(String error) { - return '重新整理失敗:$error'; - } + String get reminderAtTaskDue => 'At the task due time'; @override - String get selectOrCreateTaskList => '請選擇或建立待辦清單以開始使用。'; + String get unsupportedReminder => + 'This reminder type is preserved but its time cannot be edited.'; @override - String get signInToViewTasks => '登入以查看待辦事項。'; + String get relatedRemindersTitle => 'Keep related reminders?'; @override - String get noTasks => '沒有待辦事項。'; + String relatedRemindersDescription(int count) { + return 'This date has $count related reminders. Keep them at their current date and time?'; + } @override - String get noTasksYet => '還沒有待辦事項'; + String get discardRelatedReminders => '捨棄提醒'; @override - String get noTasksYetMessage => '建立待辦事項或重新整理帳戶以開始使用。'; + String get keepRelatedReminders => '保留提醒'; @override - String get noTasksInList => '此清單中沒有待辦事項。'; + String get addGuest => '新增參與者'; @override - String get overdue => '已逾期'; + String get addGuestEmail => '新增參與者電子郵件'; @override - String get today => '今天'; + String get removeReminder => '移除提醒'; @override - String get tomorrow => '明天'; + String get off => '關閉'; @override - String get upcoming => '即將到期'; + String get repeat => '重複'; @override - String get noDate => '無日期'; + String get repeatNone => '不重複'; @override - String get completed => '已完成'; + String get noneValue => '無'; @override - String duePrefix(String date) { - return '$date 到期'; - } + String get repeatDaily => '每天'; @override - String dateTimeDisplay(String date, String time) { - return '$date · $time'; - } + String get repeatWeekly => '每週'; @override - String get taskDetails => '待辦事項詳細資料'; + String get repeatMonthly => '每月'; @override - String get editTask => '編輯待辦事項'; + String get repeatYearly => '每年'; @override - String get noTaskSelected => '未選取待辦事項。'; + String get repeatEvery => '重複循環'; @override - String get noTaskSelectedHelper => '選擇待辦事項以查看和編輯詳細資料。'; + String get repeatOn => 'Repeat on'; @override - String get taskUnavailable => '無法使用待辦事項。'; + String get repeatEnd => '停止重複'; @override - String get signInToEditTasks => '登入以編輯待辦事項。'; + String get repeatNever => 'Never'; @override - String get refreshTask => '重新整理待辦事項'; + String get repeatUntil => 'On date'; @override - String get primarySection => '主要資訊'; + String get repeatAfter => 'After a number of occurrences'; @override - String get statusSection => '狀態'; + String get repeatCount => 'Occurrences'; @override - String get openStatus => '未完成'; + String get repeatDayOfMonth => 'Days of month'; @override - String get doneStatus => '已完成'; + String get repeatMonths => 'Months'; @override - String get notes => '備註'; + String get repeatOrdinal => 'Weekday position'; @override - String get dueDate => '到期日'; + String get repeatSpecificDays => 'Specific days'; @override - String get clearDueDate => '清除到期日'; + String get repeatFirst => 'First'; @override - String get dueTime => '到期時間'; + String get repeatSecond => 'Second'; @override - String get startDate => '開始日期'; + String get repeatThird => 'Third'; @override - String get startTime => '開始時間'; + String get repeatFourth => 'Fourth'; @override - String get endDate => '結束日期'; + String get repeatFifth => 'Fifth'; @override - String get endTime => '結束時間'; + String get repeatSecondToLast => 'Second to last'; @override - String get reminderDate => '提醒日期'; + String get repeatLast => 'Last'; @override - String get reminderTime => '提醒時間'; + String get repeatAnyDay => 'Day'; @override - String get reminder => '提醒'; + String get repeatWeekday => 'Weekday'; @override - String get addReminder => '新增提醒'; + String get repeatWeekendDay => 'Weekend day'; @override - String get addGuest => '新增參與者'; + String repeatEveryDays(int count) { + return 'Every $count days'; + } @override - String get addGuestEmail => '新增參與者電子郵件'; + String repeatEveryWeeks(int count) { + return 'Every $count weeks'; + } @override - String get removeReminder => '移除提醒'; + String repeatEveryMonths(int count) { + return 'Every $count months'; + } @override - String get off => '關閉'; + String repeatEveryYears(int count) { + return 'Every $count years'; + } @override - String get repeat => '重複'; + String repeatOnDaysSummary(String days) { + return '於 $days'; + } @override - String get repeatNone => '不重複'; + String repeatOnMonthDaysSummary(String days) { + return '於 $days 天'; + } @override - String get noneValue => '無'; + String repeatOnOrdinalSummary(String ordinal, String days) { + return 'on the $ordinal $days'; + } @override - String get repeatDaily => '每天'; + String repeatInMonthsSummary(String months) { + return '在 $months'; + } @override - String get repeatWeekly => '每週'; + String repeatTimesSummary(int count) { + return '$count 次'; + } @override - String get repeatMonthly => '每月'; + String repeatUntilSummary(String date) { + return '到 $date'; + } @override - String get repeatYearly => '每年'; + String get unsupportedRecurrencePreserved => + 'This recurrence rule uses options that this editor does not change.'; @override String get importance => '重要性'; @@ -3301,6 +4673,26 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get createSubtask => '建立子待辦事項'; + @override + String get subtasks => '子待辦事項'; + + @override + String get duplicateTask => '再製任務'; + + @override + String get taskDuplicated => 'Task duplicated.'; + + @override + String taskDuplicateFailed(String error) { + return 'Could not duplicate the task: $error'; + } + + @override + String get hideSubtasks => '隱藏子工作項目'; + + @override + String get hideClosedSubtasks => '隱藏已關閉的子工作項目'; + @override String get moveToTop => '移至頂端'; @@ -3312,7 +4704,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String deleteTaskConfirmation(String title) { - return '要從 Google Tasks 刪除「$title」嗎?'; + return '要刪除「$title」嗎?'; } @override @@ -3627,6 +5019,222 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get noLocationsFound => '找不到地點'; + @override + String get requiredField => 'This field is required.'; + + @override + String get providerConnectionDescription => + 'Connect calendars and tasks from one of these providers.'; + + @override + String get appleICloudProvider => 'Apple iCloud Calendar'; + + @override + String get nextcloudProvider => 'Nextcloud'; + + @override + String get appleICloudTasksProvider => 'Apple iCloud'; + + @override + String get nextcloudTasksProvider => 'Nextcloud Tasks'; + + @override + String get addAppleICloudAccount => 'Add Apple iCloud Calendar account'; + + @override + String get addNextcloudAccount => 'Add Nextcloud account'; + + @override + String get waitingForAppleICloud => 'Connecting to Apple iCloud…'; + + @override + String get waitingForNextcloud => 'Waiting for Nextcloud authorization…'; + + @override + String get connectAppleICloudTitle => 'Connect Apple iCloud Calendar'; + + @override + String get appleAccountEmail => 'Apple Account email'; + + @override + String get appleAppSpecificPassword => 'App-specific password'; + + @override + String get appleAppSpecificPasswordHelp => + 'Create an app-specific password after enabling two-factor authentication for your Apple Account.'; + + @override + String get appleAppSpecificPasswordResetWarning => + 'Resetting your Apple Account password revokes app-specific passwords.'; + + @override + String get connectNextcloudTitle => 'Connect Nextcloud'; + + @override + String get nextcloudServerUrl => 'Nextcloud server or CalDAV address'; + + @override + String get nextcloudServerUrlHelp => + 'Enter your Nextcloud server URL, or paste the primary CalDAV address copied from Nextcloud.'; + + @override + String get nextcloudBrowserAuthorizationHelp => + 'BusyMax will open your browser. Approve access there, then return to BusyMax.'; + + @override + String get connectAccountAction => 'Connect'; + + @override + String get cancelAccountConnection => 'Cancel connection'; + + @override + String get nextcloudAccountRemovedRevokeFailed => + 'The account was removed locally, but its Nextcloud app password could not be revoked.'; + + @override + String get davCachedOfflineNotice => + 'Calendar and task data is cached locally for offline use.'; + + @override + String get davReauthenticationRequired => + 'Reconnect this account to resume synchronization.'; + + @override + String get davTemporarilyUnavailable => + 'This account is temporarily unavailable.'; + + @override + String get davPermissionChanged => + 'Server permissions changed. Pending edits are paused.'; + + @override + String get davUnsupportedServer => + 'This server or provider profile is not supported.'; + + @override + String get collectionSettings => 'Collections'; + + @override + String get calendarContent => 'Calendar events'; + + @override + String get taskContent => 'Tasks'; + + @override + String get readOnlySharedCollection => 'Read-only or shared'; + + @override + String get pendingLocally => 'Pending locally'; + + @override + String get conflictBlocked => 'Blocked by conflict'; + + @override + String get authenticationBlocked => 'Blocked until reconnect'; + + @override + String get operationFailed => 'Operation failed'; + + @override + String get keepServerVersion => 'Keep server version'; + + @override + String get reapplyLocalChange => 'Review and reapply local change'; + + @override + String get duplicateLocalItem => 'Duplicate as new item'; + + @override + String get davConnectionState => 'Connection state'; + + @override + String get davConnected => 'Connected'; + + @override + String get davConnecting => 'Connecting…'; + + @override + String get davSignedOut => 'Signed out'; + + @override + String davLastSuccessfulSync(String time) { + return 'Last successful sync: $time'; + } + + @override + String get davNeverSynced => 'Not synchronized yet'; + + @override + String get refreshCollections => 'Refresh collections'; + + @override + String nextcloudServerHost(String host) { + return 'Server: $host'; + } + + @override + String get collectionSupportsEvents => 'Event calendar'; + + @override + String get collectionSupportsTasks => 'Task list'; + + @override + String get collectionSupportsEventsAndTasks => 'Events and tasks'; + + @override + String get writableCollection => 'Writable'; + + @override + String get sharedCollection => 'Shared'; + + @override + String collectionLastSynced(String time) { + return 'Last synchronized: $time'; + } + + @override + String collectionSyncError(String code) { + return 'Sync issue: $code'; + } + + @override + String get syncConflicts => 'Synchronization conflicts'; + + @override + String remoteChangedAt(String time) { + return 'Server changed: $time'; + } + + @override + String localPendingEdit(String summary) { + return 'Local edit: $summary'; + } + + @override + String get conflictResolutionFailed => 'The conflict could not be resolved.'; + + @override + String get recurringEventScope => 'Recurring event scope'; + + @override + String get entireSeries => 'Entire series'; + + @override + String get singleOccurrence => 'This occurrence'; + + @override + String get thisAndFutureUnavailable => 'This and future (not available)'; + + @override + String get chooseRecurringEventScope => + 'Choose whether this change applies to the entire series or only this occurrence.'; + + @override + String get taskDueBeforeStart => '到期時間不得早於開始時間。'; + + @override + String get taskStartDueTimeModeMismatch => '請同時設定開始與到期時間,或將工作設為全天。'; + @override String deleteCalendarConfirmation(String title) { return '要刪除「$title」嗎?'; diff --git a/lib/src/app/app_bootstrap.dart b/lib/src/app/app_bootstrap.dart index 29d2dc4..059af6a 100644 --- a/lib/src/app/app_bootstrap.dart +++ b/lib/src/app/app_bootstrap.dart @@ -5,10 +5,22 @@ import 'package:drift/drift.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; import '../config/build_config.dart'; import '../core/time/local_time_zone.dart'; import '../db/app_database.dart'; +import '../dav/auth/dav_account_onboarding_service.dart'; +import '../dav/auth/nextcloud_app_password_revoker.dart'; +import '../dav/auth/nextcloud_login_flow_v2.dart'; +import '../dav/dav_provider_profile.dart'; +import '../dav/discovery/dav_discovery_service.dart'; +import '../dav/http/dav_http_transport.dart'; +import '../dav/mutation/dav_conflict_repository.dart'; +import '../dav/mutation/dav_pending_operations.dart'; +import '../dav/mutation/dav_task_list_mutation_service.dart'; +import '../dav/sync/dav_account_sync_engine.dart'; +import '../dav/storage/dav_settings_repository.dart'; import '../features/calendar/data/calendar_repository.dart'; import '../features/accounts/data/accounts_repository.dart'; import '../features/auth/data/auth_repository.dart'; @@ -24,20 +36,22 @@ import '../features/sync/sync_auth_error.dart'; import '../features/sync/sync_engine.dart'; import '../features/task_lists/data/task_lists_repository.dart'; import '../features/tasks/data/tasks_repository.dart'; +import '../features/tasks/domain/task_remote_client.dart'; import '../google_tasks/api/google_tasks_api_client.dart'; import '../google_tasks/http/authenticated_http_client.dart'; import '../google_tasks/http/retrying_http_client.dart'; import '../google_tasks/oauth/oauth_loopback_flow.dart'; import '../google_tasks/oauth/oauth_service.dart'; -import '../google_tasks/oauth/oauth_token_store.dart'; -import '../google_tasks/oauth/portal_encrypted_oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/core/secrets/portal_encrypted_secret_store.dart'; import '../google_calendar/google_calendar_api_client.dart'; import '../microsoft_calendar/microsoft_calendar_api_client.dart'; import '../microsoft_todo/api/microsoft_todo_api_client.dart'; -import '../microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart'; +import '../microsoft_todo/api/microsoft_todo_task_remote_client.dart'; import '../microsoft_todo/oauth/microsoft_oauth_service.dart'; import '../platform/linux_window_service.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import '../schedule/schedule_commands.dart'; import '../schedule/schedule_repository.dart'; import 'app_router.dart'; @@ -66,6 +80,69 @@ final baseHttpClientProvider = Provider((ref) { return client; }); +final nextcloudLoginFlowV2Provider = Provider((ref) { + return NextcloudLoginFlowV2(client: ref.watch(baseHttpClientProvider)); +}); + +final davAccountOnboardingServiceProvider = + Provider((ref) { + final client = ref.watch(baseHttpClientProvider); + return DavAccountOnboardingService( + database: ref.watch(databaseProvider), + secretStore: ref.watch(secretStoreProvider), + accountsRepository: ref.watch(accountsRepositoryProvider), + nextcloudLoginFlow: ref.watch(nextcloudLoginFlowV2Provider), + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) { + final profile = davProviderProfile( + provider, + nextcloudServer: provider == BusyProvider.nextcloud + ? accountAuthority + : null, + ); + final transport = DavHttpTransport( + client: client, + profile: profile, + accountAuthority: accountAuthority, + ); + return DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: accountAuthority, + accountId: accountId, + credential: credential, + ).discover( + correlationId: const Uuid().v4(), + cancellationToken: cancellationToken, + ); + }, + nextcloudCredentialRevoker: + ({required accountId, required credential}) { + final profile = davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: credential.canonicalServer, + ); + return NextcloudAppPasswordRevoker( + transport: DavHttpTransport( + client: client, + profile: profile, + accountAuthority: credential.canonicalServer, + ), + ).revoke( + accountId: accountId, + credential: credential, + correlationId: const Uuid().v4(), + ); + }, + ); + }); + final retryingHttpClientProvider = Provider((ref) { return RetryingHttpClient(inner: ref.watch(baseHttpClientProvider)); }); @@ -80,20 +157,20 @@ final feedbackSubmissionServiceProvider = Provider(( ); }); -final oAuthTokenStoreProvider = Provider((ref) { +final secretStoreProvider = Provider((ref) { if (Platform.isLinux && (Platform.environment['SNAP']?.isNotEmpty ?? false)) { // flutter_secure_storage_linux warms up direct libsecret first, so // SECRET_BACKEND=file cannot avoid a locked keyring inside strict snaps. - return PortalEncryptedOAuthTokenStore(); + return PortalEncryptedSecretStore(); } - return SecureOAuthTokenStore(ref.watch(secureStorageProvider)); + return SecureSecretStore(ref.watch(secureStorageProvider)); }); final applicationOAuthServiceProvider = Provider((ref) { return OAuthService( config: ref.watch(buildConfigProvider), httpClient: ref.watch(baseHttpClientProvider), - tokenStore: ref.watch(oAuthTokenStoreProvider), + tokenStore: ref.watch(secretStoreProvider), loopbackFlow: OAuthLoopbackFlow(), ); }); @@ -108,7 +185,7 @@ final microsoftOAuthServiceProvider = Provider((ref) { return MicrosoftOAuthService( config: ref.watch(buildConfigProvider), httpClient: ref.watch(baseHttpClientProvider), - tokenStore: ref.watch(oAuthTokenStoreProvider), + tokenStore: ref.watch(secretStoreProvider), loopbackFlow: OAuthLoopbackFlow(), ); }); @@ -173,6 +250,49 @@ final accountManagementStreamProvider = StreamProvider>(( return ref.watch(accountsRepositoryProvider).watchVisibleAccounts(); }); +final davSettingsRepositoryProvider = Provider((ref) { + return DavSettingsRepository( + database: ref.watch(databaseProvider), + onVisibilityChanged: (_) => + ref.read(notificationSchedulerProvider).checkNow(), + ); +}); + +final davCollectionsStreamProvider = + StreamProvider>((ref) { + return ref.watch(davSettingsRepositoryProvider).watchCollections(); + }); + +final davConflictRepositoryProvider = Provider((ref) { + return DavConflictRepository(database: ref.watch(databaseProvider)); +}); + +final davConflictsStreamProvider = StreamProvider>(( + ref, +) { + return ref.watch(davConflictRepositoryProvider).watchUnresolved(); +}); + +final davConflictResolutionServiceProvider = + Provider((ref) { + final database = ref.watch(databaseProvider); + return DavConflictResolutionService( + database: database, + pendingQueue: DavPendingOperationQueue(database: database), + ); + }); + +final davTaskCollectionCapabilitiesProvider = + FutureProvider.family< + TaskCollectionCapabilities?, + ({String accountId, String taskListId}) + >((ref, key) async { + final collection = await ref + .watch(davSettingsRepositoryProvider) + .collectionByTaskListId(key.accountId, key.taskListId); + return collection?.taskCapabilities; + }); + final selectedAccountIdProvider = StateProvider((ref) => null); final selectedAccountProvider = Provider((ref) { @@ -191,17 +311,18 @@ final selectedAccountProvider = Provider((ref) { return null; }); -final selectedAccountCapabilitiesProvider = Provider(( - ref, -) { - final account = ref.watch(selectedAccountProvider); - return capabilitiesForProvider(account?.provider ?? TaskProvider.google); -}); +final selectedAccountCapabilitiesProvider = + Provider((ref) { + final account = ref.watch(selectedAccountProvider); + return account == null + ? noTaskCollectionCapabilities + : adapterDefaultTaskCapabilities(account.provider); + }); final localTimeZoneProvider = Provider((ref) => localIanaTimeZone()); final googleTasksApiClientForAccountProvider = - Provider.family((ref, accountId) { + Provider.family((ref, accountId) { final config = ref.watch(buildConfigProvider); return GoogleTasksRestApiClient( httpClient: ref.watch(retryingHttpClientProvider), @@ -261,16 +382,16 @@ final microsoftCalendarApiClientForAccountProvider = ); }); -final microsoftAsGoogleTasksApiClientForAccountProvider = - Provider.family((ref, accountId) { - return MicrosoftTodoGoogleTasksAdapter( +final microsoftTodoTaskRemoteClientForAccountProvider = + Provider.family((ref, accountId) { + return MicrosoftTodoTaskRemoteClient( client: ref.watch(microsoftTodoApiClientForAccountProvider(accountId)), defaultTimeZone: ref.watch(localTimeZoneProvider), ); }); final taskRemoteApiClientForAccountProvider = - Provider.family((ref, accountId) { + Provider.family((ref, accountId) { final accounts = ref.watch(accountsStreamProvider).valueOrNull; AccountEntity? account; for (final candidate in accounts ?? const []) { @@ -283,35 +404,40 @@ final taskRemoteApiClientForAccountProvider = return null; } return switch (account.provider) { - TaskProvider.microsoft => ref.watch( - microsoftAsGoogleTasksApiClientForAccountProvider(accountId), + BusyProvider.microsoft => ref.watch( + microsoftTodoTaskRemoteClientForAccountProvider(accountId), ), - TaskProvider.google => ref.watch( + BusyProvider.google => ref.watch( googleTasksApiClientForAccountProvider(accountId), ), + BusyProvider.appleICloud || BusyProvider.nextcloud => null, }; }); typedef SyncEngineForAccountFactory = - SyncEngine Function(String accountId, TaskProvider provider); + SyncEngine Function(String accountId, BusyProvider provider); final syncEngineForAccountFactoryProvider = Provider((ref) { return (accountId, provider) { final apiClient = switch (provider) { - TaskProvider.microsoft => ref.read( - microsoftAsGoogleTasksApiClientForAccountProvider(accountId), + BusyProvider.microsoft => ref.read( + microsoftTodoTaskRemoteClientForAccountProvider(accountId), ), - TaskProvider.google => ref.read( + BusyProvider.google => ref.read( googleTasksApiClientForAccountProvider(accountId), ), + BusyProvider.appleICloud || + BusyProvider.nextcloud => throw StateError( + 'DAV task accounts must use DavSynchronizationEngine.', + ), }; return SyncEngine( database: ref.read(databaseProvider), apiClient: apiClient, accountId: accountId, - fullRefreshOnly: provider == TaskProvider.microsoft, + fullRefreshOnly: provider == BusyProvider.microsoft, onConflictBlocked: ref .read(desktopNotificationServiceProvider) .notifyConflict, @@ -320,18 +446,22 @@ final syncEngineForAccountFactoryProvider = }); typedef CalendarSyncEngineForAccountFactory = - CalendarSyncEngine Function(String accountId, TaskProvider provider); + CalendarSyncEngine Function(String accountId, BusyProvider provider); final calendarSyncEngineForAccountFactoryProvider = Provider((ref) { return (accountId, provider) { final client = switch (provider) { - TaskProvider.microsoft => ref.read( + BusyProvider.microsoft => ref.read( microsoftCalendarApiClientForAccountProvider(accountId), ), - TaskProvider.google => ref.read( + BusyProvider.google => ref.read( googleCalendarApiClientForAccountProvider(accountId), ), + BusyProvider.appleICloud || + BusyProvider.nextcloud => throw StateError( + 'DAV calendar accounts must use DavSynchronizationEngine.', + ), }; return CalendarSyncEngine( database: ref.read(databaseProvider), @@ -346,10 +476,28 @@ final calendarSyncEngineForAccountFactoryProvider = }; }); +typedef DavAccountSyncEngineFactory = + DavAccountSyncEngine Function(String accountId); + +final davAccountSyncEngineFactoryProvider = + Provider((ref) { + return (accountId) => DavAccountSyncEngine( + database: ref.read(databaseProvider), + secretStore: ref.read(secretStoreProvider), + httpClient: ref.read(baseHttpClientProvider), + accountId: accountId, + rebuildNotifications: (accountId, affectedObjectIds) => + ref.read(notificationSchedulerProvider).checkNow(), + reportPendingMutationFailure: (accountId, error) => ref + .read(desktopNotificationServiceProvider) + .notifySyncFailure(error.safeMessage), + ); + }); + final accountSyncOperationsProvider = Provider((ref) { final accountsRepository = ref.watch(accountsRepositoryProvider); - Future providerForAccount(String accountId) async { + Future providerForAccount(String accountId) async { final account = await accountsRepository.accountById(accountId); if (account == null) { throw StateError('Account $accountId is unavailable.'); @@ -357,8 +505,18 @@ final accountSyncOperationsProvider = Provider((ref) { return account.provider; } - return DelegatingAccountSyncOperations( - syncTasks: (accountId, {required full}) async { + return RoutingAccountSyncOperations( + usesDav: (accountId) async { + final provider = await providerForAccount(accountId); + return provider == BusyProvider.appleICloud || + provider == BusyProvider.nextcloud; + }, + syncDav: (accountId, {required full}) async { + await ref + .read(davAccountSyncEngineFactoryProvider)(accountId) + .synchronize(full: full); + }, + syncTasksRest: (accountId, {required full}) async { final provider = await providerForAccount(accountId); final engine = ref.read(syncEngineForAccountFactoryProvider)( accountId, @@ -370,7 +528,7 @@ final accountSyncOperationsProvider = Provider((ref) { await engine.incrementalSync(); } }, - syncCalendar: (accountId, {required full}) async { + syncCalendarRest: (accountId, {required full}) async { final provider = await providerForAccount(accountId); final engine = ref.read(calendarSyncEngineForAccountFactoryProvider)( accountId, @@ -418,7 +576,7 @@ final allAccountsSyncRunnerProvider = Provider((ref) { return runAllSignedInAccountSync( listSignedInAccounts: ref .read(accountsRepositoryProvider) - .listSignedInAccounts, + .listSyncEligibleAccounts, syncAccount: syncAccount, onSyncFailure: ref .read(desktopNotificationServiceProvider) @@ -466,7 +624,7 @@ final Provider activeAccountProvider = Provider((ref) { return session.isSignedIn ? session.accountId : null; }); -final googleTasksApiClientProvider = Provider((ref) { +final googleTasksApiClientProvider = Provider((ref) { final accountId = ref.watch(activeAccountProvider); if (accountId == null) { return null; @@ -474,6 +632,19 @@ final googleTasksApiClientProvider = Provider((ref) { return ref.watch(taskRemoteApiClientForAccountProvider(accountId)); }); +final davTaskListMutationClientForAccountProvider = + Provider.family((ref, accountId) { + return DavTaskListMutationService( + database: ref.watch(databaseProvider), + secretStore: ref.watch(secretStoreProvider), + httpClient: ref.watch(baseHttpClientProvider), + accountId: accountId, + refreshAfterMutation: () => ref + .read(accountSyncOperationsProvider) + .syncAccount(accountId, full: true), + ); + }); + final taskListsRepositoryProvider = Provider((ref) { final accountId = ref.watch(activeAccountProvider); if (accountId == null) { @@ -483,6 +654,9 @@ final taskListsRepositoryProvider = Provider((ref) { database: ref.watch(databaseProvider), accountId: accountId, apiClient: ref.watch(googleTasksApiClientProvider), + davMutationClient: ref.watch( + davTaskListMutationClientForAccountProvider(accountId), + ), onMutationQueued: ref.watch(pendingMutationSyncRequesterProvider)?.request, ); }); @@ -492,6 +666,13 @@ final taskListsRepositoryForAccountProvider = return TaskListsRepository( database: ref.watch(databaseProvider), accountId: accountId, + apiClient: ref.watch(taskRemoteApiClientForAccountProvider(accountId)), + davMutationClient: ref.watch( + davTaskListMutationClientForAccountProvider(accountId), + ), + onMutationQueued: ref + .watch(pendingMutationSyncRequesterForAccountProvider(accountId)) + .request, ); }); @@ -535,7 +716,7 @@ final Provider syncEngineProvider = Provider((ref) { database: ref.watch(databaseProvider), apiClient: apiClient, accountId: accountId, - fullRefreshOnly: account!.provider == TaskProvider.microsoft, + fullRefreshOnly: account!.provider == BusyProvider.microsoft, onConflictBlocked: ref .watch(desktopNotificationServiceProvider) .notifyConflict, diff --git a/lib/src/app/busymax_dialogs.dart b/lib/src/app/busymax_dialogs.dart index 1acfd79..5d1fdef 100644 --- a/lib/src/app/busymax_dialogs.dart +++ b/lib/src/app/busymax_dialogs.dart @@ -147,12 +147,14 @@ Future showBusyMaxModalEditorDialog( BuildContext context, { required WidgetBuilder builder, LinuxHeaderBarService? headerBarService, + Color? barrierColor, double maxWidth = BusyMaxSizes.compactDetailsWidth, double? maxHeight = 760, }) async { return showBusyMaxModalDialog( context, headerBarService: headerBarService, + barrierColor: barrierColor, barrierDismissible: false, builder: (dialogContext) { return BusyMaxModalEditorSurface( diff --git a/lib/src/calendar_providers/calendar_colors.dart b/lib/src/calendar_providers/calendar_colors.dart index 3766468..21af0b2 100644 --- a/lib/src/calendar_providers/calendar_colors.dart +++ b/lib/src/calendar_providers/calendar_colors.dart @@ -1,4 +1,4 @@ -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; String? calendarSourceBackgroundColorHex({ required BusyProvider provider, @@ -9,7 +9,7 @@ String? calendarSourceBackgroundColorHex({ if (explicitColor != null) { return explicitColor; } - if (provider == TaskProvider.microsoft) { + if (provider == BusyProvider.microsoft) { return microsoftCalendarColorHex(colorId); } return null; diff --git a/lib/src/calendar_providers/calendar_sync_dto.dart b/lib/src/calendar_providers/calendar_sync_dto.dart index 277694d..911ed3c 100644 --- a/lib/src/calendar_providers/calendar_sync_dto.dart +++ b/lib/src/calendar_providers/calendar_sync_dto.dart @@ -1,4 +1,4 @@ -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; class CalendarSourceDto { const CalendarSourceDto({ diff --git a/lib/src/calendar_providers/cloud_calendar_client.dart b/lib/src/calendar_providers/cloud_calendar_client.dart index e365ac3..fe1c90b 100644 --- a/lib/src/calendar_providers/cloud_calendar_client.dart +++ b/lib/src/calendar_providers/cloud_calendar_client.dart @@ -1,4 +1,4 @@ -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'calendar_mutation.dart'; import 'calendar_provider_capabilities.dart'; import 'calendar_sync_dto.dart'; diff --git a/lib/src/config/build_config.dart b/lib/src/config/build_config.dart index 10227b0..df0974d 100644 --- a/lib/src/config/build_config.dart +++ b/lib/src/config/build_config.dart @@ -93,8 +93,13 @@ class BuildConfig { useFakeProviderData || googleOAuthClientId.trim().isNotEmpty; bool get hasMicrosoftOAuthClientId => !useFakeProviderData && microsoftOAuthClientId.trim().isNotEmpty; + bool get hasAppleICloudProvider => !useFakeProviderData; + bool get hasNextcloudProvider => !useFakeProviderData; bool get hasAnyProviderConfigured => - hasGoogleOAuthClientId || hasMicrosoftOAuthClientId; + hasGoogleOAuthClientId || + hasMicrosoftOAuthClientId || + hasAppleICloudProvider || + hasNextcloudProvider; String get missingClientIdMessage { if (kReleaseMode) { diff --git a/lib/src/google_tasks/oauth/oauth_models.dart b/lib/src/core/auth/oauth_models.dart similarity index 97% rename from lib/src/google_tasks/oauth/oauth_models.dart rename to lib/src/core/auth/oauth_models.dart index 6af132c..5a27de5 100644 --- a/lib/src/google_tasks/oauth/oauth_models.dart +++ b/lib/src/core/auth/oauth_models.dart @@ -1,3 +1,4 @@ +/// OAuth token record shared by the Google and Microsoft adapters. class OAuthTokenSet { const OAuthTokenSet({ required this.accessToken, diff --git a/lib/src/core/logging/redacting_logger.dart b/lib/src/core/logging/redacting_logger.dart index 8261685..33d50d4 100644 --- a/lib/src/core/logging/redacting_logger.dart +++ b/lib/src/core/logging/redacting_logger.dart @@ -3,41 +3,76 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -final _sensitivePatterns = [ - RegExp(r'Bearer\s+[A-Za-z0-9._~+/=-]+', caseSensitive: false), - RegExp(r'(^|[?&\s])access_token=[^&\s]+', caseSensitive: false), - RegExp(r'(^|[?&\s])refresh_token=[^&\s]+', caseSensitive: false), - // RFC 7009 uses the generic `token` field. Restrict this pattern to URL - // query syntax so ordinary prose such as "token=value" remains readable. - RegExp(r'([?&])token=[^&\s]+', caseSensitive: false), - RegExp(r'(^|[?&\s])client_secret=[^&\s]+', caseSensitive: false), - RegExp(r'"client_secret"\s*:\s*"[^"]*"', caseSensitive: false), - RegExp(r'client_secret\s*:\s*[^,\n\s]+', caseSensitive: false), - RegExp(r'(^|[?&\s])code_verifier=[^&\s]+', caseSensitive: false), - RegExp(r'(^|[?&\s])code=[^&\s]+', caseSensitive: false), - RegExp(r'Authorization:\s*[^,\n]+', caseSensitive: false), -]; - String redactForLog(Object? value) { var text = value?.toString() ?? ''; - for (final pattern in _sensitivePatterns) { - text = text.replaceAllMapped(pattern, (match) { - final source = match.group(0) ?? ''; - if (source.contains('=')) { - return '${source.split('=').first}=[REDACTED]'; - } - if (source.trimLeft().startsWith('"')) { - return '"client_secret":"[REDACTED]"'; - } - if (source.toLowerCase().contains('client_secret')) { - return 'client_secret: [REDACTED]'; - } - if (source.toLowerCase().startsWith('authorization:')) { - return 'Authorization: [REDACTED]'; - } - return 'Bearer [REDACTED]'; - }); - } + text = text.replaceAllMapped( + RegExp(r'\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+', caseSensitive: false), + (match) => '${match.group(1)} [REDACTED]', + ); + text = text.replaceAllMapped( + RegExp( + r'\bAuthorization\s*[:=]\s*(?:(Bearer|Basic)\s+)?[^,\r\n}]+', + caseSensitive: false, + ), + (match) => + 'Authorization: ${match.group(1) == null ? '' : '${match.group(1)} '}' + '[REDACTED]', + ); + text = text.replaceAllMapped( + RegExp(r'\b(?:Set-)?Cookie\s*[:=]\s*[^\r\n]+', caseSensitive: false), + (match) => + '${(match.group(0) ?? '').split(RegExp(r'[:=]')).first}: ' + '[REDACTED]', + ); + text = text.replaceAllMapped( + RegExp(r'(https?://)[^/@\s]+@', caseSensitive: false), + (match) => '${match.group(1)}[REDACTED]@', + ); + text = text.replaceAllMapped( + RegExp( + r'(^|[\s])((?:access|refresh|id)[_-]?token|code|code[_-]?verifier|client[_-]?secret|password|app[_-]?password|app[_-]?specific[_-]?password|poll[_-]?token)=([^&\s]+)', + caseSensitive: false, + multiLine: true, + ), + (match) => '${match.group(1)}${match.group(2)}=[REDACTED]', + ); + text = text.replaceAllMapped( + RegExp( + r'([?&])((?:access|refresh|id)[_-]?token|token|code|code_verifier|client_secret|password|app[_-]?password|ticket|session|key)=([^&#\s]+)', + caseSensitive: false, + ), + (match) => '${match.group(1)}${match.group(2)}=[REDACTED]', + ); + text = text.replaceAllMapped( + RegExp( + r'"(accessToken|refreshToken|idToken|token|clientSecret|client_secret|codeVerifier|appPassword|appSpecificPassword|password|pollToken|login|loginUrl|requestBody|responseBody|body|cookie)"\s*:\s*(?:"(?:\\.|[^"])*"|[^,}\s]+)', + caseSensitive: false, + ), + (match) => '"${match.group(1)}":"[REDACTED]"', + ); + text = text.replaceAllMapped( + RegExp( + r'\b(access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|code[_-]?verifier|app[_-]?password|app[_-]?specific[_-]?password|password|poll[_-]?token|loginUrl|requestBody|responseBody|cookie)\s*([:=])\s*(?:"(?:\\.|[^"])*"|\x27[^\x27]*\x27|[^,}\s&#]+)', + caseSensitive: false, + ), + (match) => + '${match.group(1)}${match.group(2)}' + '${match.group(2) == '=' ? '' : ' '}[REDACTED]', + ); + text = text.replaceAllMapped( + RegExp( + r'\b(href|requestUri|objectUri|collectionUri)\s*([:=])\s*[^,}\s]+', + caseSensitive: false, + ), + (match) => '${match.group(1)}${match.group(2)} [REDACTED]', + ); + text = text.replaceAllMapped( + RegExp( + r'\b(SUMMARY|DESCRIPTION|LOCATION|ATTENDEE|ORGANIZER|COMMENT|CONTACT|ATTACH|X-ALT-DESC)(?:;[^:\r\n]*)?:[^\r\n]*', + caseSensitive: false, + ), + (match) => '${match.group(1)}:[REDACTED]', + ); return text; } diff --git a/lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart b/lib/src/core/secrets/portal_encrypted_secret_store.dart similarity index 68% rename from lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart rename to lib/src/core/secrets/portal_encrypted_secret_store.dart index 7a111f6..a807f6e 100644 --- a/lib/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart +++ b/lib/src/core/secrets/portal_encrypted_secret_store.dart @@ -7,22 +7,24 @@ import 'package:cryptography/cryptography.dart'; import 'package:dbus/dbus.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:posix/posix.dart' show chmod; -import '../../core/logging/redacting_logger.dart'; -import 'oauth_models.dart'; -import 'oauth_token_store.dart'; +import '../../providers/busy_provider.dart'; +import '../auth/oauth_models.dart'; +import '../logging/redacting_logger.dart'; +import 'secret_store.dart'; const _encryptedTokenStoreVersion = 1; -class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { - PortalEncryptedOAuthTokenStore({ +class PortalEncryptedSecretStore implements SecretStore { + PortalEncryptedSecretStore({ SecretPortalClient? portalClient, File? storageFile, RedactingLogger? logger, }) : _portalClient = portalClient ?? XdgSecretPortalClient(), _storageFile = storageFile ?? _defaultStorageFile(), _logger = - logger ?? RedactingLogger(Logger('PortalEncryptedOAuthTokenStore')); + logger ?? RedactingLogger(Logger('PortalEncryptedSecretStore')); final SecretPortalClient _portalClient; final File _storageFile; @@ -32,53 +34,45 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { PortalSecret? _cachedSecret; var _loggedRuntime = false; - static const _activeAccountKey = SecureOAuthTokenStore.activeAccountKey; + static const _activeAccountKey = SecureSecretStore.activeAccountKey; + static const _legacyActiveAccountKey = + SecureSecretStore.legacyActiveAccountKey; static const _kdfInfo = 'io.busystack.busymax.oauth-token-store.v1'; @override - Future readActiveAccountId() => _read(_activeAccountKey); + Future readActiveAccountId() async { + final current = await _read(_activeAccountKey); + if (current != null) { + return current; + } + final legacy = await _read(_legacyActiveAccountKey); + if (legacy == null) { + return null; + } + await _write(_activeAccountKey, legacy); + if (await _read(_activeAccountKey) != legacy) { + throw const SecretStoreException( + 'SecretStoreMigrationVerificationFailed', + 'The active account secret migration could not be verified.', + ); + } + await _delete(_legacyActiveAccountKey); + return legacy; + } @override - Future readTokenSet(String accountId) async { - final accessToken = await _read(_key(accountId, 'access_token')); - final expiresAtText = await _read(_key(accountId, 'expires_at_utc')); - if (accessToken == null || expiresAtText == null) { + Future readCredential(String accountId) async { + final serialized = await _read(_credentialKey(accountId)); + if (serialized == null) { return null; } - - final refreshToken = await _read(_key(accountId, 'refresh_token')); - final tokenType = await _read(_key(accountId, 'token_type')); - final scopeText = await _read(_key(accountId, 'scope')); - final idToken = await _read(_key(accountId, 'id_token')); - - return OAuthTokenSet( - accessToken: accessToken, - refreshToken: refreshToken, - idToken: idToken, - expiresAtUtc: DateTime.parse(expiresAtText).toUtc(), - tokenType: tokenType ?? 'Bearer', - scopes: (scopeText ?? '') - .split(RegExp(r'\s+')) - .where((scope) => scope.isNotEmpty) - .toSet(), - ); + return _decodePortalCredential(serialized); } @override - Future saveTokenSet(String accountId, OAuthTokenSet tokenSet) async { + Future saveCredential(String accountId, SecretRecord credential) async { final values = await _readAll('write'); - values[_key(accountId, 'access_token')] = tokenSet.accessToken; - if (tokenSet.refreshToken != null) { - values[_key(accountId, 'refresh_token')] = tokenSet.refreshToken!; - } - if (tokenSet.idToken != null) { - values[_key(accountId, 'id_token')] = tokenSet.idToken!; - } - values[_key(accountId, 'expires_at_utc')] = tokenSet.expiresAtUtc - .toUtc() - .toIso8601String(); - values[_key(accountId, 'token_type')] = tokenSet.tokenType; - values[_key(accountId, 'scope')] = tokenSet.scopes.join(' '); + values[_credentialKey(accountId)] = jsonEncode(credential.toJson()); await _writeAll(values, 'write'); } @@ -88,21 +82,62 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { } @override - Future clearTokenSet(String accountId) async { + Future deleteCredential(String accountId) async { final values = await _readAll('delete'); - final beforeLength = values.length; - values.remove(_key(accountId, 'access_token')); - values.remove(_key(accountId, 'refresh_token')); - values.remove(_key(accountId, 'id_token')); - values.remove(_key(accountId, 'expires_at_utc')); - values.remove(_key(accountId, 'token_type')); - values.remove(_key(accountId, 'scope')); - if (values.length == beforeLength) { + if (values.remove(_credentialKey(accountId)) == null) { return; } await _writeAll(values, 'delete'); } + @override + Future migrateLegacyOAuthCredential( + String accountId, + BusyProvider provider, + ) async { + if (provider != BusyProvider.google && provider != BusyProvider.microsoft) { + return false; + } + final values = await _readAll('migrate'); + if (values.containsKey(_credentialKey(accountId))) { + return false; + } + final accessToken = values[_legacyKey(accountId, 'access_token')]; + final expiresAt = values[_legacyKey(accountId, 'expires_at_utc')]; + if (accessToken == null || expiresAt == null) { + return false; + } + final record = OAuthSecretRecord( + provider: provider, + tokenSet: OAuthTokenSet( + accessToken: accessToken, + refreshToken: values[_legacyKey(accountId, 'refresh_token')], + idToken: values[_legacyKey(accountId, 'id_token')], + expiresAtUtc: DateTime.parse(expiresAt).toUtc(), + tokenType: values[_legacyKey(accountId, 'token_type')] ?? 'Bearer', + scopes: (values[_legacyKey(accountId, 'scope')] ?? '') + .split(RegExp(r'\s+')) + .where((scope) => scope.isNotEmpty) + .toSet(), + ), + ); + values[_credentialKey(accountId)] = jsonEncode(record.toJson()); + await _writeAll(values, 'migrate-write'); + final verified = await readOAuthTokenSet(accountId, provider); + if (verified == null || verified.accessToken != accessToken) { + throw const SecretStoreException( + 'SecretStoreMigrationVerificationFailed', + 'The OAuth credential migration could not be verified.', + ); + } + final migrated = await _readAll('migrate-delete'); + for (final name in _legacyOAuthFieldNames) { + migrated.remove(_legacyKey(accountId, name)); + } + await _writeAll(migrated, 'migrate-delete'); + return true; + } + @override Future clearActiveAccount() { return _delete(_activeAccountKey); @@ -135,6 +170,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { } try { + await _restrictExistingStoragePermissions(); final envelope = _asStringObjectMap( jsonDecode(await _storageFile.readAsString()), ); @@ -152,7 +188,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { final clearBytes = await _cipher.decrypt(box, secretKey: key); return _asStringMap(jsonDecode(utf8.decode(clearBytes))); } on Object catch (error) { - if (error is OAuthException) { + if (error is SecretStoreException) { rethrow; } throw _storageException(operation, error); @@ -163,6 +199,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { _logRuntime(); try { await _storageFile.parent.create(recursive: true); + _restrictPermissions(_storageFile.parent.path, '700'); final existing = await _readEnvelopeIfPresent(); final salt = existing == null ? _randomBytes(16) @@ -190,10 +227,24 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { 'portal_token': secret.token, }; final tempFile = File('${_storageFile.path}.tmp'); + final tempType = await FileSystemEntity.type( + tempFile.path, + followLinks: false, + ); + if (tempType == FileSystemEntityType.notFound) { + await tempFile.create(exclusive: true); + } else if (tempType != FileSystemEntityType.file) { + throw FileSystemException( + 'The encrypted credential temporary path is not a regular file.', + tempFile.path, + ); + } + _restrictPermissions(tempFile.path, '600'); await tempFile.writeAsString(jsonEncode(envelope), flush: true); await tempFile.rename(_storageFile.path); + _restrictPermissions(_storageFile.path, '600'); } on Object catch (error) { - if (error is OAuthException) { + if (error is SecretStoreException) { rethrow; } throw _storageException(operation, error); @@ -204,9 +255,31 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { if (!await _storageFile.exists()) { return null; } + await _restrictExistingStoragePermissions(); return _asStringObjectMap(jsonDecode(await _storageFile.readAsString())); } + Future _restrictExistingStoragePermissions() async { + final type = await FileSystemEntity.type( + _storageFile.path, + followLinks: false, + ); + if (type != FileSystemEntityType.file) { + throw FileSystemException( + 'The encrypted credential path is not a regular file.', + _storageFile.path, + ); + } + _restrictPermissions(_storageFile.parent.path, '700'); + _restrictPermissions(_storageFile.path, '600'); + } + + void _restrictPermissions(String path, String permissions) { + if (Platform.isLinux || Platform.isMacOS) { + chmod(path, permissions); + } + } + Future _retrieveSecret( String operation, { String? token, @@ -219,7 +292,7 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { final secret = await _portalClient.retrieveSecret(token: token); _cachedSecret = secret; _logger.info( - 'Secure token storage portal retrieve succeeded: ' + 'Secure credential storage portal retrieve succeeded: ' 'operation=$operation snap=${_isRunningInSnap()} ' 'secret_backend=${_secretBackendLabel()} has_portal_token=${secret.token != null}', ); @@ -243,26 +316,28 @@ class PortalEncryptedOAuthTokenStore implements OAuthTokenStore { } _loggedRuntime = true; _logger.info( - 'Secure token storage runtime: backend=xdg-secret-portal-file ' + 'Secure credential storage runtime: backend=xdg-secret-portal-file ' 'snap=${_isRunningInSnap()} secret_backend=${_secretBackendLabel()}', ); } - OAuthException _storageException(String operation, Object error) { + SecretStoreException _storageException(String operation, Object error) { _logger.warning( - 'Secure token storage $operation failed: ' + 'Secure credential storage $operation failed: ' '${sanitizedSecureStorageError(error)}', ); - if (error is OAuthException) { + if (error is SecretStoreException) { return error; } - return const OAuthException( - 'OAuthSecureStorageUnavailable', - secureTokenStorageUnavailableMessage, + return const SecretStoreException( + 'SecretStoreUnavailable', + secretStorageUnavailableMessage, ); } - String _key(String accountId, String name) => + String _credentialKey(String accountId) => 'busymax.secret.$accountId.v1'; + + String _legacyKey(String accountId, String name) => 'busymax.oauth.$accountId.$name'; } @@ -447,6 +522,25 @@ Map _asStringMap(Object? value) { }); } +SecretRecord _decodePortalCredential(String serialized) { + try { + final decoded = jsonDecode(serialized); + if (decoded is! Map) { + throw const SecretStoreCorruptException( + 'The encrypted credential record is not a JSON object.', + ); + } + return SecretRecord.fromJson(decoded.cast()); + } on SecretStoreException { + rethrow; + } on Object catch (error) { + throw SecretStoreCorruptException( + 'The encrypted credential record could not be decoded ' + '(${error.runtimeType}).', + ); + } +} + Map _asStringObjectMap(Object? value) { if (value is! Map) { throw const FormatException('Encrypted token store envelope is not a map.'); @@ -480,6 +574,15 @@ List _randomBytes(int length) { return List.generate(length, (_) => random.nextInt(256)); } +const _legacyOAuthFieldNames = [ + 'access_token', + 'refresh_token', + 'id_token', + 'expires_at_utc', + 'token_type', + 'scope', +]; + String _requestToken() { final bytes = _randomBytes(16); final hex = bytes diff --git a/lib/src/core/secrets/secret_store.dart b/lib/src/core/secrets/secret_store.dart new file mode 100644 index 0000000..0e9d083 --- /dev/null +++ b/lib/src/core/secrets/secret_store.dart @@ -0,0 +1,516 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:logging/logging.dart'; + +import '../../providers/busy_provider.dart'; +import '../../providers/account_authority.dart'; +import '../auth/oauth_models.dart'; +import '../logging/redacting_logger.dart'; + +const secretRecordSchemaVersion = 1; + +enum CredentialKind { oauth, appleAppSpecificPassword, nextcloudAppPassword } + +extension CredentialKindValue on CredentialKind { + String get storageValue => switch (this) { + CredentialKind.oauth => 'oauth', + CredentialKind.appleAppSpecificPassword => 'apple_app_specific_password', + CredentialKind.nextcloudAppPassword => 'nextcloud_app_password', + }; +} + +sealed class SecretRecord { + const SecretRecord({required this.provider, required this.kind}); + + final BusyProvider provider; + final CredentialKind kind; + + Map toJson(); + + static SecretRecord fromJson(Map json) { + if (json['schemaVersion'] != secretRecordSchemaVersion) { + throw SecretStoreCorruptException( + 'Unsupported credential schema version ${json['schemaVersion']}.', + ); + } + final provider = BusyProviderCodec.requireStorageValue( + json['provider']?.toString(), + ); + return switch (json['kind']) { + 'oauth' => OAuthSecretRecord( + provider: provider, + tokenSet: OAuthTokenSet( + accessToken: _requiredSecretString(json, 'accessToken'), + refreshToken: _optionalSecretString(json, 'refreshToken'), + idToken: _optionalSecretString(json, 'idToken'), + expiresAtUtc: DateTime.parse( + _requiredSecretString(json, 'expiresAtUtc'), + ).toUtc(), + tokenType: _requiredSecretString(json, 'tokenType'), + scopes: _stringList(json['scopes']).toSet(), + ), + ), + 'apple_app_specific_password' => AppleICloudSecretRecord( + username: _requiredSecretString(json, 'username'), + appSpecificPassword: _requiredSecretString(json, 'appSpecificPassword'), + ), + 'nextcloud_app_password' => NextcloudSecretRecord( + canonicalServer: Uri.parse( + _requiredSecretString(json, 'canonicalServer'), + ), + loginName: _requiredSecretString(json, 'loginName'), + appPassword: _requiredSecretString(json, 'appPassword'), + ), + final Object? value => throw SecretStoreCorruptException( + 'Unsupported credential kind $value.', + ), + }; + } + + @override + String toString() => + '$runtimeType(provider: ${provider.storageValue}, secret: [REDACTED])'; +} + +final class OAuthSecretRecord extends SecretRecord { + const OAuthSecretRecord({required super.provider, required this.tokenSet}) + : assert( + provider == BusyProvider.google || provider == BusyProvider.microsoft, + ), + super(kind: CredentialKind.oauth); + + final OAuthTokenSet tokenSet; + + @override + Map toJson() => { + 'schemaVersion': secretRecordSchemaVersion, + 'kind': kind.storageValue, + 'provider': provider.storageValue, + 'accessToken': tokenSet.accessToken, + if (tokenSet.refreshToken != null) 'refreshToken': tokenSet.refreshToken, + if (tokenSet.idToken != null) 'idToken': tokenSet.idToken, + 'expiresAtUtc': tokenSet.expiresAtUtc.toUtc().toIso8601String(), + 'tokenType': tokenSet.tokenType, + 'scopes': tokenSet.scopes.toList()..sort(), + }; +} + +final class AppleICloudSecretRecord extends SecretRecord { + AppleICloudSecretRecord({ + required String username, + required String appSpecificPassword, + }) : username = username.trim(), + appSpecificPassword = appSpecificPassword.trim(), + super( + provider: BusyProvider.appleICloud, + kind: CredentialKind.appleAppSpecificPassword, + ) { + if (this.username.isEmpty || this.appSpecificPassword.isEmpty) { + throw const SecretStoreCorruptException( + 'Apple iCloud credentials must not be empty.', + ); + } + } + + final String username; + final String appSpecificPassword; + + @override + Map toJson() => { + 'schemaVersion': secretRecordSchemaVersion, + 'kind': kind.storageValue, + 'provider': provider.storageValue, + 'username': username, + 'appSpecificPassword': appSpecificPassword, + }; +} + +final class NextcloudSecretRecord extends SecretRecord { + NextcloudSecretRecord({ + required Uri canonicalServer, + required String loginName, + required String appPassword, + }) : canonicalServer = Uri.parse( + normalizeNextcloudServerAuthority(canonicalServer.toString()), + ), + loginName = loginName.trim(), + appPassword = appPassword.trim(), + super( + provider: BusyProvider.nextcloud, + kind: CredentialKind.nextcloudAppPassword, + ) { + if (canonicalServer.scheme != 'https' || + canonicalServer.host.isEmpty || + canonicalServer.userInfo.isNotEmpty || + this.loginName.isEmpty || + this.appPassword.isEmpty) { + throw const SecretStoreCorruptException( + 'Nextcloud credentials contain an invalid server or empty value.', + ); + } + } + + final Uri canonicalServer; + final String loginName; + final String appPassword; + + @override + Map toJson() => { + 'schemaVersion': secretRecordSchemaVersion, + 'kind': kind.storageValue, + 'provider': provider.storageValue, + 'canonicalServer': canonicalServer.toString(), + 'loginName': loginName, + 'appPassword': appPassword, + }; +} + +abstract interface class SecretStore { + Future readActiveAccountId(); + Future setActiveAccountId(String accountId); + Future clearActiveAccount(); + Future readCredential(String accountId); + Future saveCredential(String accountId, SecretRecord credential); + Future deleteCredential(String accountId); + + /// Performs the one-time legacy OAuth key migration for an existing account. + /// Returns true only when legacy values were replaced and then deleted. + Future migrateLegacyOAuthCredential( + String accountId, + BusyProvider provider, + ); +} + +extension OAuthSecretStoreAccess on SecretStore { + Future readOAuthTokenSet( + String accountId, + BusyProvider expectedProvider, + ) async { + final credential = await readCredential(accountId); + if (credential == null) { + return null; + } + if (credential case OAuthSecretRecord( + provider: final provider, + tokenSet: final tokenSet, + ) when provider == expectedProvider) { + return tokenSet; + } + throw SecretStoreCredentialMismatchException( + accountId: accountId, + expectedProvider: expectedProvider, + actualProvider: credential.provider, + actualKind: credential.kind, + ); + } + + Future saveOAuthTokenSet( + String accountId, + BusyProvider provider, + OAuthTokenSet tokenSet, + ) { + return saveCredential( + accountId, + OAuthSecretRecord(provider: provider, tokenSet: tokenSet), + ); + } +} + +class SecureSecretStore implements SecretStore { + SecureSecretStore(this._storage, {RedactingLogger? logger}) + : _logger = logger ?? RedactingLogger(Logger('SecureSecretStore')); + + final FlutterSecureStorage _storage; + final RedactingLogger _logger; + var _loggedRuntime = false; + + static const activeAccountKey = 'busymax.secret.active_account_id'; + static const legacyActiveAccountKey = 'busymax.oauth.active_account_id'; + + @override + Future readActiveAccountId() async { + final current = await _read(activeAccountKey); + if (current != null) { + return current; + } + final legacy = await _read(legacyActiveAccountKey); + if (legacy == null) { + return null; + } + await _write(activeAccountKey, legacy); + if (await _read(activeAccountKey) != legacy) { + throw const SecretStoreException( + 'SecretStoreMigrationVerificationFailed', + 'The active account secret migration could not be verified.', + ); + } + await _delete(legacyActiveAccountKey); + return legacy; + } + + @override + Future readCredential(String accountId) async { + final serialized = await _read(_credentialKey(accountId)); + if (serialized == null) { + return null; + } + return _decodeCredential(serialized); + } + + @override + Future saveCredential(String accountId, SecretRecord credential) async { + await _write(_credentialKey(accountId), jsonEncode(credential.toJson())); + } + + @override + Future setActiveAccountId(String accountId) { + return _write(activeAccountKey, accountId); + } + + @override + Future deleteCredential(String accountId) { + return _delete(_credentialKey(accountId)); + } + + @override + Future clearActiveAccount() => _delete(activeAccountKey); + + @override + Future migrateLegacyOAuthCredential( + String accountId, + BusyProvider provider, + ) async { + if (provider != BusyProvider.google && provider != BusyProvider.microsoft) { + return false; + } + if (await readCredential(accountId) != null) { + return false; + } + final accessToken = await _read(_legacyKey(accountId, 'access_token')); + final expiresAtText = await _read(_legacyKey(accountId, 'expires_at_utc')); + if (accessToken == null || expiresAtText == null) { + return false; + } + final tokenSet = OAuthTokenSet( + accessToken: accessToken, + refreshToken: await _read(_legacyKey(accountId, 'refresh_token')), + idToken: await _read(_legacyKey(accountId, 'id_token')), + expiresAtUtc: DateTime.parse(expiresAtText).toUtc(), + tokenType: await _read(_legacyKey(accountId, 'token_type')) ?? 'Bearer', + scopes: (await _read(_legacyKey(accountId, 'scope')) ?? '') + .split(RegExp(r'\s+')) + .where((scope) => scope.isNotEmpty) + .toSet(), + ); + await saveOAuthTokenSet(accountId, provider, tokenSet); + final verified = await readOAuthTokenSet(accountId, provider); + if (verified == null || verified.accessToken != tokenSet.accessToken) { + throw const SecretStoreException( + 'SecretStoreMigrationVerificationFailed', + 'The OAuth credential migration could not be verified.', + ); + } + await _deleteLegacyOAuthKeys(accountId); + return true; + } + + String _credentialKey(String accountId) => 'busymax.secret.$accountId.v1'; + + String _legacyKey(String accountId, String name) => + 'busymax.oauth.$accountId.$name'; + + Future _deleteLegacyOAuthKeys(String accountId) async { + for (final name in _legacyOAuthFieldNames) { + await _delete(_legacyKey(accountId, name)); + } + } + + Future _read(String key) async { + _logRuntime(); + try { + return await _storage.read(key: key); + } on PlatformException catch (error) { + throw _secureStorageException('read', error); + } + } + + Future _write(String key, String? value) async { + _logRuntime(); + try { + await _storage.write(key: key, value: value); + } on PlatformException catch (error) { + throw _secureStorageException('write', error); + } + } + + Future _delete(String key) async { + _logRuntime(); + try { + await _storage.delete(key: key); + } on PlatformException catch (error) { + throw _secureStorageException('delete', error); + } + } + + void _logRuntime() { + if (_loggedRuntime) { + return; + } + _loggedRuntime = true; + _logger.info( + 'Secret storage runtime: backend=flutter-secure-storage ' + 'snap=${_isRunningInSnap()} secret_backend=${_secretBackendLabel()}', + ); + } + + SecretStoreException _secureStorageException( + String operation, + PlatformException error, + ) { + _logger.warning( + 'Secret storage $operation failed: ' + '${sanitizedFlutterSecureStorageError(error)}', + ); + return const SecretStoreException( + 'SecretStoreUnavailable', + secretStorageUnavailableMessage, + ); + } +} + +class InMemorySecretStore implements SecretStore { + final _credentials = {}; + String? _activeAccountId; + + @override + Future clearActiveAccount() async => _activeAccountId = null; + + @override + Future deleteCredential(String accountId) async { + _credentials.remove(accountId); + } + + @override + Future migrateLegacyOAuthCredential( + String accountId, + BusyProvider provider, + ) async => false; + + @override + Future readActiveAccountId() async => _activeAccountId; + + @override + Future readCredential(String accountId) async => + _credentials[accountId]; + + @override + Future saveCredential(String accountId, SecretRecord credential) async { + _credentials[accountId] = credential; + } + + @override + Future setActiveAccountId(String accountId) async { + _activeAccountId = accountId; + } +} + +class SecretStoreException implements Exception { + const SecretStoreException(this.code, this.message); + + final String code; + final String message; + + @override + String toString() => '$code: $message'; +} + +class SecretStoreCorruptException extends SecretStoreException { + const SecretStoreCorruptException(String message) + : super('SecretStoreCorrupt', message); +} + +class SecretStoreCredentialMismatchException extends SecretStoreException { + SecretStoreCredentialMismatchException({ + required this.accountId, + required this.expectedProvider, + required this.actualProvider, + required this.actualKind, + }) : super( + 'SecretStoreCredentialMismatch', + 'The stored credential does not match the requested account provider.', + ); + + final String accountId; + final BusyProvider expectedProvider; + final BusyProvider actualProvider; + final CredentialKind actualKind; +} + +const secretStorageUnavailableMessage = + 'Secure credential storage is locked. Unlock your system keyring and try again.'; + +String sanitizedFlutterSecureStorageError(PlatformException error) { + final message = redactForLog(error.message).replaceAll(RegExp(r'\s+'), ' '); + return 'domain=flutter_secure_storage_linux code=${error.code} ' + 'message=${message.trim()} details_type=${error.details.runtimeType}'; +} + +SecretRecord _decodeCredential(String serialized) { + try { + final decoded = jsonDecode(serialized); + if (decoded is! Map) { + throw const SecretStoreCorruptException( + 'The credential record is not a JSON object.', + ); + } + return SecretRecord.fromJson(decoded.cast()); + } on SecretStoreException { + rethrow; + } on Object catch (error) { + throw SecretStoreCorruptException( + 'The credential record could not be decoded (${error.runtimeType}).', + ); + } +} + +String _requiredSecretString(Map json, String key) { + final value = json[key]?.toString(); + if (value == null || value.isEmpty) { + throw SecretStoreCorruptException('Credential record is missing $key.'); + } + return value; +} + +String? _optionalSecretString(Map json, String key) { + final value = json[key]?.toString(); + return value == null || value.isEmpty ? null : value; +} + +List _stringList(Object? value) { + if (value is! List) { + return const []; + } + return value.map((entry) => entry.toString()).toList(growable: false); +} + +const _legacyOAuthFieldNames = [ + 'access_token', + 'refresh_token', + 'id_token', + 'expires_at_utc', + 'token_type', + 'scope', +]; + +bool _isRunningInSnap() => Platform.environment['SNAP']?.isNotEmpty ?? false; + +String _secretBackendLabel() { + final backend = Platform.environment['SECRET_BACKEND']; + if (backend == null || backend.isEmpty) { + return ''; + } + return backend == 'file' ? 'file' : ''; +} diff --git a/lib/src/dav/auth/dav_account_dialogs.dart b/lib/src/dav/auth/dav_account_dialogs.dart new file mode 100644 index 0000000..34f04c3 --- /dev/null +++ b/lib/src/dav/auth/dav_account_dialogs.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; + +import '../../app/busymax_design.dart'; +import '../../app/busymax_dialogs.dart'; +import '../../l10n/l10n.dart'; +import '../../platform/linux_header_bar_service.dart'; + +final class AppleICloudCredentialInput { + AppleICloudCredentialInput({required this.email, required this.password}); + + final String email; + final String password; + + @override + String toString() => + 'AppleICloudCredentialInput(email: [REDACTED], password: [REDACTED])'; +} + +Future showAppleICloudCredentialDialog( + BuildContext context, { + String? fixedEmail, + LinuxHeaderBarService? headerBarService, +}) { + return showBusyMaxModalDialog( + context, + headerBarService: headerBarService, + barrierDismissible: false, + builder: (context) => _AppleCredentialDialog(fixedEmail: fixedEmail), + ); +} + +Future showNextcloudServerDialog( + BuildContext context, { + String? initialServer, + LinuxHeaderBarService? headerBarService, +}) { + return showBusyMaxModalDialog( + context, + headerBarService: headerBarService, + barrierDismissible: false, + builder: (context) => _NextcloudServerDialog(initialServer: initialServer), + ); +} + +final class _AppleCredentialDialog extends StatefulWidget { + const _AppleCredentialDialog({required this.fixedEmail}); + + final String? fixedEmail; + + @override + State<_AppleCredentialDialog> createState() => _AppleCredentialDialogState(); +} + +final class _AppleCredentialDialogState extends State<_AppleCredentialDialog> { + late final TextEditingController _email = TextEditingController( + text: widget.fixedEmail ?? '', + ); + final TextEditingController _password = TextEditingController(); + final FocusNode _emailFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + var _submitted = false; + + @override + void dispose() { + _email.dispose(); + _password.dispose(); + _emailFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final emailMissing = _submitted && _email.text.trim().isEmpty; + final passwordMissing = _submitted && _password.text.trim().isEmpty; + return BusyMaxDialogShell( + title: l10n.connectAppleICloudTitle, + maxWidth: 520, + actions: [ + BusyMaxPushButton.standard( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + BusyMaxPushButton.suggested( + onPressed: _submit, + child: Text(l10n.connectAccountAction), + ), + ], + children: [ + Text(l10n.appleAppSpecificPasswordHelp), + const SizedBox(height: BusyMaxSpacing.md), + TextField( + key: const Key('apple-account-email-field'), + controller: _email, + focusNode: _emailFocus, + autofocus: widget.fixedEmail == null, + readOnly: widget.fixedEmail != null, + keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.username, AutofillHints.email], + textInputAction: TextInputAction.next, + onSubmitted: (_) => _passwordFocus.requestFocus(), + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: l10n.appleAccountEmail, + errorText: emailMissing ? l10n.requiredField : null, + ), + ), + const SizedBox(height: BusyMaxSpacing.md), + Semantics( + textField: true, + label: l10n.appleAppSpecificPassword, + child: TextField( + key: const Key('apple-app-specific-password-field'), + controller: _password, + focusNode: _passwordFocus, + autofocus: widget.fixedEmail != null, + obscureText: true, + enableSuggestions: false, + autocorrect: false, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + onSubmitted: (_) => _submit(), + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: l10n.appleAppSpecificPassword, + errorText: passwordMissing ? l10n.requiredField : null, + ), + ), + ), + const SizedBox(height: BusyMaxSpacing.md), + Text( + l10n.appleAppSpecificPasswordResetWarning, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: BusyMaxSpacing.sm), + Text( + l10n.davCachedOfflineNotice, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + } + + void _submit() { + setState(() => _submitted = true); + final email = _email.text.trim(); + final password = _password.text.trim(); + if (email.isEmpty || password.isEmpty) return; + Navigator.of( + context, + ).pop(AppleICloudCredentialInput(email: email, password: password)); + } +} + +final class _NextcloudServerDialog extends StatefulWidget { + const _NextcloudServerDialog({required this.initialServer}); + + final String? initialServer; + + @override + State<_NextcloudServerDialog> createState() => _NextcloudServerDialogState(); +} + +final class _NextcloudServerDialogState extends State<_NextcloudServerDialog> { + late final TextEditingController _server = TextEditingController( + text: widget.initialServer ?? '', + ); + var _submitted = false; + + @override + void dispose() { + _server.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BusyMaxDialogShell( + title: l10n.connectNextcloudTitle, + maxWidth: 520, + actions: [ + BusyMaxPushButton.standard( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + BusyMaxPushButton.suggested( + onPressed: _submit, + child: Text(l10n.connectAccountAction), + ), + ], + children: [ + TextField( + key: const Key('nextcloud-server-field'), + controller: _server, + autofocus: true, + keyboardType: TextInputType.url, + autofillHints: const [AutofillHints.url], + textInputAction: TextInputAction.done, + onChanged: (_) => setState(() {}), + onSubmitted: (_) => _submit(), + decoration: InputDecoration( + labelText: l10n.nextcloudServerUrl, + hintText: 'https://cloud.example.com/remote.php/dav', + helperText: l10n.nextcloudServerUrlHelp, + helperMaxLines: 2, + errorText: _submitted && _server.text.trim().isEmpty + ? l10n.requiredField + : null, + ), + ), + const SizedBox(height: BusyMaxSpacing.md), + Text(l10n.nextcloudBrowserAuthorizationHelp), + const SizedBox(height: BusyMaxSpacing.sm), + Text( + l10n.davCachedOfflineNotice, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + } + + void _submit() { + setState(() => _submitted = true); + final server = _server.text.trim(); + if (server.isEmpty) return; + Navigator.of(context).pop(server); + } +} diff --git a/lib/src/dav/auth/dav_account_onboarding_service.dart b/lib/src/dav/auth/dav_account_onboarding_service.dart new file mode 100644 index 0000000..1f01ae4 --- /dev/null +++ b/lib/src/dav/auth/dav_account_onboarding_service.dart @@ -0,0 +1,468 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../core/secrets/secret_store.dart'; +import '../../db/app_database.dart'; +import '../../features/accounts/data/accounts_repository.dart'; +import '../../providers/account_authority.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../discovery/dav_discovery_models.dart'; +import '../discovery/dav_discovery_repository.dart'; +import '../http/dav_http_transport.dart'; +import 'nextcloud_login_flow_v2.dart'; + +typedef DavOnboardingDiscovery = + Future Function({ + required String accountId, + required BusyProvider provider, + required Uri accountAuthority, + required DavBasicCredential credential, + DavCancellationToken? cancellationToken, + }); + +typedef NextcloudCredentialRevoker = + Future Function({ + required String accountId, + required NextcloudSecretRecord credential, + }); + +final class DavAccountConnectionResult { + const DavAccountConnectionResult({ + required this.accountId, + required this.discovery, + }); + + final String accountId; + final DavDiscoveryResult discovery; +} + +final class DavAccountRemovalResult { + const DavAccountRemovalResult({ + required this.remoteRevocationAttempted, + required this.remoteRevocationSucceeded, + required this.remoteFailureCode, + }); + + final bool remoteRevocationAttempted; + final bool remoteRevocationSucceeded; + final String? remoteFailureCode; +} + +final class DavAccountOnboardingService { + DavAccountOnboardingService({ + required AppDatabase database, + required SecretStore secretStore, + required DavOnboardingDiscovery discover, + required NextcloudLoginFlowV2 nextcloudLoginFlow, + AccountsRepository? accountsRepository, + DavDiscoveryRepository? discoveryRepository, + NextcloudCredentialRevoker? nextcloudCredentialRevoker, + String Function()? idFactory, + DateTime Function()? nowUtc, + }) : _database = database, + _secretStore = secretStore, + _discover = discover, + _nextcloudLoginFlow = nextcloudLoginFlow, + _accountsRepository = + accountsRepository ?? AccountsRepository(database: database), + _discoveryRepository = + discoveryRepository ?? DavDiscoveryRepository(database: database), + _nextcloudCredentialRevoker = nextcloudCredentialRevoker, + _idFactory = idFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final AppDatabase _database; + final SecretStore _secretStore; + final DavOnboardingDiscovery _discover; + final NextcloudLoginFlowV2 _nextcloudLoginFlow; + final AccountsRepository _accountsRepository; + final DavDiscoveryRepository _discoveryRepository; + final NextcloudCredentialRevoker? _nextcloudCredentialRevoker; + final String Function() _idFactory; + final DateTime Function() _nowUtc; + + Future connectAppleICloud({ + required String email, + required String appSpecificPassword, + DavCancellationToken? cancellationToken, + }) { + final credential = AppleICloudSecretRecord( + username: email, + appSpecificPassword: appSpecificPassword, + ); + return _connect( + provider: BusyProvider.appleICloud, + authority: Uri.parse(appleICloudAccountAuthority), + providerAccountId: credential.username, + credential: credential, + basicCredential: DavBasicCredential( + username: credential.username, + password: credential.appSpecificPassword, + ), + calendarsEnabled: true, + tasksEnabled: false, + email: credential.username, + displayName: credential.username, + cancellationToken: cancellationToken, + ); + } + + Future connectNextcloud({ + required String enteredServer, + DavCancellationToken? cancellationToken, + }) async { + final login = await _nextcloudLoginFlow.start(enteredServer); + cancellationToken?.throwIfCancelled(); + return _connectNextcloudLogin(login, cancellationToken: cancellationToken); + } + + Future reconnectNextcloud({ + required String accountId, + required String enteredServer, + DavCancellationToken? cancellationToken, + }) async { + final account = await _accountsRepository.accountById(accountId); + if (account == null || account.provider != BusyProvider.nextcloud) { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavNextcloudAccountUnavailable', + safeMessage: 'The Nextcloud account could not be reconnected.', + ); + } + final login = await _nextcloudLoginFlow.start(enteredServer); + cancellationToken?.throwIfCancelled(); + final authority = normalizeAccountAuthority( + BusyProvider.nextcloud, + authority: login.canonicalServer.toString(), + ); + final providerId = normalizeProviderAccountId( + BusyProvider.nextcloud, + login.loginName, + ); + if (authority != account.authority || + providerId != account.providerAccountId) { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavNextcloudReconnectIdentityMismatch', + safeMessage: 'Nextcloud returned a different account during reconnect.', + ); + } + final result = await _connectNextcloudLogin( + login, + cancellationToken: cancellationToken, + ); + if (result.accountId != accountId) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavNextcloudReconnectAccountMismatch', + safeMessage: 'The Nextcloud account identity could not be restored.', + ); + } + return result; + } + + Future _connectNextcloudLogin( + NextcloudLoginFlowResult login, { + DavCancellationToken? cancellationToken, + }) { + final credential = NextcloudSecretRecord( + canonicalServer: login.canonicalServer, + loginName: login.loginName, + appPassword: login.appPassword, + ); + return _connect( + provider: BusyProvider.nextcloud, + authority: credential.canonicalServer, + providerAccountId: credential.loginName, + credential: credential, + basicCredential: DavBasicCredential( + username: credential.loginName, + password: credential.appPassword, + ), + calendarsEnabled: true, + tasksEnabled: true, + displayName: credential.loginName, + cancellationToken: cancellationToken, + ); + } + + void cancelNextcloudLogin() => _nextcloudLoginFlow.cancel(); + + Future replaceAppleAppSpecificPassword({ + required String accountId, + required String appSpecificPassword, + DavCancellationToken? cancellationToken, + }) async { + final account = await _accountsRepository.accountById(accountId); + final previous = await _secretStore.readCredential(accountId); + if (account == null || + account.provider != BusyProvider.appleICloud || + previous is! AppleICloudSecretRecord) { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavAppleAccountUnavailable', + safeMessage: 'The Apple iCloud account could not be reconnected.', + ); + } + final replacement = AppleICloudSecretRecord( + username: previous.username, + appSpecificPassword: appSpecificPassword, + ); + final discovery = await _discover( + accountId: accountId, + provider: BusyProvider.appleICloud, + accountAuthority: Uri.parse(appleICloudAccountAuthority), + credential: DavBasicCredential( + username: replacement.username, + password: replacement.appSpecificPassword, + ), + cancellationToken: cancellationToken, + ); + _validateDiscovery( + discovery, + accountId: accountId, + provider: BusyProvider.appleICloud, + ); + await _replaceCredentialVerified(accountId, replacement, previous); + try { + await _database.transaction(() async { + await _accountsRepository.upsertSignedInAccount( + id: accountId, + provider: BusyProvider.appleICloud, + providerAccountId: account.providerAccountId, + authority: appleICloudAccountAuthority, + credentialKind: CredentialKind.appleAppSpecificPassword, + displayName: account.displayName, + email: account.email, + grantedScopes: '', + calendarsEnabled: true, + tasksEnabled: false, + ); + await _discoveryRepository.commitSuccessfulInventory(discovery); + await _requeueAuthenticationBlockedOperations(accountId); + }); + return DavAccountConnectionResult( + accountId: accountId, + discovery: discovery, + ); + } on Object { + await _secretStore.saveCredential(accountId, previous); + rethrow; + } + } + + Future removeAccount(String accountId) async { + final account = await _accountsRepository.accountById(accountId); + if (account == null) { + return const DavAccountRemovalResult( + remoteRevocationAttempted: false, + remoteRevocationSucceeded: false, + remoteFailureCode: null, + ); + } + final credential = await _secretStore.readCredential(accountId); + var attempted = false; + var succeeded = false; + String? failureCode; + if (account.provider == BusyProvider.nextcloud && + credential is NextcloudSecretRecord && + _nextcloudCredentialRevoker != null) { + attempted = true; + try { + await _nextcloudCredentialRevoker( + accountId: accountId, + credential: credential, + ); + succeeded = true; + } on DavException catch (error) { + failureCode = error.code; + } on Object { + failureCode = 'NextcloudAppPasswordRevocationFailed'; + } + } + + // Local removal is deliberately independent of remote revocation success. + await _secretStore.deleteCredential(accountId); + if (await _secretStore.readActiveAccountId() == accountId) { + await _secretStore.clearActiveAccount(); + } + await _accountsRepository.deleteAccount(accountId); + return DavAccountRemovalResult( + remoteRevocationAttempted: attempted, + remoteRevocationSucceeded: succeeded, + remoteFailureCode: failureCode, + ); + } + + Future _connect({ + required BusyProvider provider, + required Uri authority, + required String providerAccountId, + required SecretRecord credential, + required DavBasicCredential basicCredential, + required bool calendarsEnabled, + required bool tasksEnabled, + required String displayName, + String? email, + DavCancellationToken? cancellationToken, + }) async { + final normalizedAuthority = normalizeAccountAuthority( + provider, + authority: authority.toString(), + ); + final normalizedProviderAccountId = normalizeProviderAccountId( + provider, + providerAccountId, + ); + final existing = + await (_database.select(_database.accounts)..where( + (row) => + row.provider.equals(provider.storageValue) & + row.authority.equals(normalizedAuthority) & + row.providerAccountId.equals(normalizedProviderAccountId), + )) + .getSingleOrNull(); + final accountId = + existing?.id ?? '${provider.storageValue}:${_idFactory()}'; + final previousCredential = existing == null + ? null + : await _secretStore.readCredential(accountId); + + // No durable credential or connected account state is written until the + // supplied credential has completed principal/home/collection discovery. + final discovery = await _discover( + accountId: accountId, + provider: provider, + accountAuthority: Uri.parse(normalizedAuthority), + credential: basicCredential, + cancellationToken: cancellationToken, + ); + _validateDiscovery(discovery, accountId: accountId, provider: provider); + await _replaceCredentialVerified(accountId, credential, previousCredential); + try { + await _database.transaction(() async { + await _accountsRepository.upsertSignedInAccount( + id: accountId, + provider: provider, + providerAccountId: normalizedProviderAccountId, + authority: normalizedAuthority, + credentialKind: credential.kind, + displayName: displayName, + email: email, + grantedScopes: '', + calendarsEnabled: calendarsEnabled, + tasksEnabled: tasksEnabled, + ); + await _discoveryRepository.commitSuccessfulInventory(discovery); + await _requeueAuthenticationBlockedOperations(accountId); + }); + await _secretStore.setActiveAccountId(accountId); + return DavAccountConnectionResult( + accountId: accountId, + discovery: discovery, + ); + } on Object { + if (previousCredential != null) { + await _secretStore.saveCredential(accountId, previousCredential); + } else { + await _secretStore.deleteCredential(accountId); + await _accountsRepository.deleteAccount(accountId); + } + rethrow; + } + } + + Future _replaceCredentialVerified( + String accountId, + SecretRecord replacement, + SecretRecord? previous, + ) async { + await _secretStore.saveCredential(accountId, replacement); + try { + final readBack = await _secretStore.readCredential(accountId); + if (!_sameCredential(readBack, replacement)) { + throw const SecretStoreException( + 'SecretStoreWriteVerificationFailed', + 'The credential could not be verified after secure storage.', + ); + } + } on Object { + if (previous != null) { + await _secretStore.saveCredential(accountId, previous); + } else { + await _secretStore.deleteCredential(accountId); + } + rethrow; + } + } + + Future _requeueAuthenticationBlockedOperations(String accountId) async { + await (_database.update(_database.pendingOps)..where( + (row) => + row.accountId.equals(accountId) & + row.operationType.like('dav.%') & + row.state.equals('auth_blocked'), + )) + .write( + PendingOpsCompanion( + state: const Value('pending'), + retryClassification: const Value('credential_replaced'), + attemptCount: const Value(0), + nextAttemptAtUtc: const Value(null), + lastErrorCode: const Value(null), + lastErrorMessage: const Value(null), + lastError: const Value(null), + updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()), + ), + ); + } +} + +void _validateDiscovery( + DavDiscoveryResult discovery, { + required String accountId, + required BusyProvider provider, +}) { + if (discovery.accountId != accountId || + discovery.provider != provider || + !discovery.service.capabilities.hasPrincipal || + !discovery.service.capabilities.hasCalendarHome) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavDiscoveryIdentityMismatch', + safeMessage: 'DAV discovery returned an invalid account identity.', + ); + } +} + +bool _sameCredential(SecretRecord? left, SecretRecord right) => + switch ((left, right)) { + ( + AppleICloudSecretRecord( + username: final leftUser, + appSpecificPassword: final leftPassword, + ), + AppleICloudSecretRecord( + username: final rightUser, + appSpecificPassword: final rightPassword, + ), + ) => + leftUser == rightUser && leftPassword == rightPassword, + ( + NextcloudSecretRecord( + canonicalServer: final leftServer, + loginName: final leftUser, + appPassword: final leftPassword, + ), + NextcloudSecretRecord( + canonicalServer: final rightServer, + loginName: final rightUser, + appPassword: final rightPassword, + ), + ) => + leftServer == rightServer && + leftUser == rightUser && + leftPassword == rightPassword, + _ => false, + }; diff --git a/lib/src/dav/auth/nextcloud_app_password_revoker.dart b/lib/src/dav/auth/nextcloud_app_password_revoker.dart new file mode 100644 index 0000000..8c0d51f --- /dev/null +++ b/lib/src/dav/auth/nextcloud_app_password_revoker.dart @@ -0,0 +1,58 @@ +import '../../core/secrets/secret_store.dart'; +import '../dav_errors.dart'; +import '../http/dav_http_transport.dart'; + +final class NextcloudAppPasswordRevoker { + const NextcloudAppPasswordRevoker({required DavHttpTransport transport}) + : _transport = transport; + + final DavHttpTransport _transport; + + Future revoke({ + required String accountId, + required NextcloudSecretRecord credential, + required String correlationId, + }) async { + final endpoint = _appendPath( + credential.canonicalServer, + 'ocs/v2.php/core/apppassword', + ); + final response = await _transport.send( + DavRequest( + method: 'DELETE', + uri: endpoint, + accountId: accountId, + correlationId: correlationId, + headers: const {'ocs-apirequest': 'true', 'accept': 'application/json'}, + retryClass: DavRetryClass.never, + ), + credential: DavBasicCredential( + username: credential.loginName, + password: credential.appPassword, + ), + ); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw DavException( + kind: response.statusCode == 401 + ? DavErrorKind.authentication + : response.statusCode == 403 + ? DavErrorKind.authorization + : response.statusCode >= 500 + ? DavErrorKind.server + : DavErrorKind.protocol, + code: 'NextcloudAppPasswordRevocationFailed', + safeMessage: 'The Nextcloud app password could not be revoked.', + statusCode: response.statusCode, + correlationId: correlationId, + retryAfter: parseDavRetryAfter(response.headers['retry-after']), + ); + } + } +} + +Uri _appendPath(Uri base, String suffix) { + final path = base.path.isEmpty + ? '/$suffix' + : '${base.path.endsWith('/') ? base.path : '${base.path}/'}$suffix'; + return base.replace(path: path, query: null, fragment: null); +} diff --git a/lib/src/dav/auth/nextcloud_login_flow_v2.dart b/lib/src/dav/auth/nextcloud_login_flow_v2.dart new file mode 100644 index 0000000..234ce64 --- /dev/null +++ b/lib/src/dav/auth/nextcloud_login_flow_v2.dart @@ -0,0 +1,460 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +import '../../providers/account_authority.dart'; +import '../dav_errors.dart'; +import '../http/dav_http_transport.dart'; + +typedef NextcloudBrowserLauncher = Future Function(Uri loginUri); +typedef NextcloudLoginDelay = Future Function(Duration duration); + +final class NextcloudLoginFlowResult { + NextcloudLoginFlowResult({ + required this.canonicalServer, + required String loginName, + required String appPassword, + }) : loginName = loginName.trim(), + appPassword = appPassword.trim() { + if (this.loginName.isEmpty || this.appPassword.isEmpty) { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'NextcloudLoginFlowMalformedCredential', + safeMessage: 'Nextcloud returned an incomplete app credential.', + ); + } + } + + final Uri canonicalServer; + final String loginName; + final String appPassword; + + @override + String toString() => 'NextcloudLoginFlowResult(credentials: [REDACTED])'; +} + +final class NextcloudLoginFlowV2 { + NextcloudLoginFlowV2({ + required http.Client client, + NextcloudBrowserLauncher? browserLauncher, + NextcloudLoginDelay? delay, + DateTime Function()? nowUtc, + this.pollInterval = const Duration(seconds: 1), + this.operationTimeout = const Duration(minutes: 10), + this.responseTimeout = const Duration(seconds: 30), + this.maximumResponseBytes = 64 * 1024, + this.maximumRedirects = 3, + }) : _client = client, + _browserLauncher = browserLauncher ?? _launchExternalBrowser, + _delay = delay ?? Future.delayed, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()) { + if (pollInterval <= Duration.zero || + operationTimeout <= Duration.zero || + responseTimeout <= Duration.zero || + maximumResponseBytes < 1 || + maximumRedirects < 0) { + throw ArgumentError('Nextcloud Login Flow limits must be positive.'); + } + } + + final http.Client _client; + final NextcloudBrowserLauncher _browserLauncher; + final NextcloudLoginDelay _delay; + final DateTime Function() _nowUtc; + final Duration pollInterval; + final Duration operationTimeout; + final Duration responseTimeout; + final int maximumResponseBytes; + final int maximumRedirects; + + Future? _activeOperation; + DavCancellationToken? _activeCancellation; + + Future start(String enteredServer) { + if (_activeOperation != null) { + throw const DavException( + kind: DavErrorKind.conflict, + code: 'NextcloudLoginFlowAlreadyRunning', + safeMessage: 'A Nextcloud connection is already in progress.', + ); + } + final cancellation = DavCancellationToken(); + _activeCancellation = cancellation; + final operation = _run(enteredServer, cancellation); + _activeOperation = operation; + unawaited( + operation.then( + (_) => _clear(operation), + onError: (_, _) => _clear(operation), + ), + ); + return operation; + } + + void cancel() => _activeCancellation?.cancel(); + + Future _run( + String enteredServer, + DavCancellationToken cancellation, + ) async { + final baseServer = normalizeNextcloudLoginServer(enteredServer); + final startUri = _appendPath(baseServer, 'index.php/login/v2'); + final deadline = _nowUtc().toUtc().add(operationTimeout); + cancellation.throwIfCancelled(); + final startResponse = await _postFollowingRedirects( + startUri, + body: null, + trustedBase: baseServer, + cancellation: cancellation, + ); + if (startResponse.statusCode != HttpStatus.ok) { + throw DavException( + kind: startResponse.statusCode >= 500 + ? DavErrorKind.server + : DavErrorKind.authentication, + code: 'NextcloudLoginFlowStartRejected', + safeMessage: 'Nextcloud could not start browser authorization.', + statusCode: startResponse.statusCode, + ); + } + final startJson = _jsonObject(startResponse.bodyBytes); + final pollJson = _jsonObjectValue(startJson, 'poll'); + final token = _requiredJsonString(pollJson, 'token'); + final pollEndpoint = _validatedFlowUri( + _requiredJsonString(pollJson, 'endpoint'), + responseUri: startResponse.requestUri, + installationPath: baseServer.path, + allowQuery: false, + ); + final loginUri = _validatedFlowUri( + _requiredJsonString(startJson, 'login'), + responseUri: startResponse.requestUri, + installationPath: baseServer.path, + allowQuery: true, + ); + cancellation.throwIfCancelled(); + final launched = await _browserLauncher(loginUri); + if (!launched) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'NextcloudLoginBrowserLaunchFailed', + safeMessage: 'The Nextcloud sign-in page could not be opened.', + ); + } + + while (true) { + cancellation.throwIfCancelled(); + if (!_nowUtc().toUtc().isBefore(deadline)) { + throw const DavException( + kind: DavErrorKind.timeout, + code: 'NextcloudLoginFlowExpired', + safeMessage: 'The Nextcloud sign-in request expired.', + ); + } + final pollResponse = await _postFollowingRedirects( + pollEndpoint, + body: {'token': token}, + trustedBase: pollEndpoint.replace(path: baseServer.path), + cancellation: cancellation, + ); + if (pollResponse.statusCode == HttpStatus.notFound) { + await _delay(pollInterval); + continue; + } + if (pollResponse.statusCode != HttpStatus.ok) { + throw DavException( + kind: pollResponse.statusCode >= 500 + ? DavErrorKind.server + : DavErrorKind.authentication, + code: 'NextcloudLoginFlowRejected', + safeMessage: 'Nextcloud did not complete browser authorization.', + statusCode: pollResponse.statusCode, + ); + } + final completed = _jsonObject(pollResponse.bodyBytes); + final normalizedAuthority = normalizeNextcloudServerAuthority( + _requiredJsonString(completed, 'server'), + ); + return NextcloudLoginFlowResult( + canonicalServer: Uri.parse(normalizedAuthority), + loginName: _requiredJsonString(completed, 'loginName'), + appPassword: _requiredJsonString(completed, 'appPassword'), + ); + } + } + + Future<_FlowResponse> _postFollowingRedirects( + Uri initialUri, { + required Map? body, + required Uri trustedBase, + required DavCancellationToken cancellation, + }) async { + var current = initialUri; + final visited = {}; + for (var redirects = 0; redirects <= maximumRedirects; redirects += 1) { + cancellation.throwIfCancelled(); + if (!visited.add(current.toString())) { + throw _redirectError('NextcloudLoginRedirectLoop'); + } + if (!_safeHttps(current) || + !_sameOrigin(current, trustedBase) || + !_withinInstallationPath(current.path, trustedBase.path)) { + throw _redirectError('NextcloudLoginRedirectRejected'); + } + final request = http.Request('POST', current) + ..followRedirects = false + ..headers['accept'] = 'application/json'; + if (body != null) { + request.headers['content-type'] = + 'application/x-www-form-urlencoded; charset=utf-8'; + request.body = _formEncode(body); + } + http.StreamedResponse streamed; + try { + streamed = await _client.send(request).timeout(responseTimeout); + } on TimeoutException { + throw const DavException( + kind: DavErrorKind.timeout, + code: 'NextcloudLoginResponseTimeout', + safeMessage: 'The Nextcloud login server did not respond in time.', + ); + } on HandshakeException { + throw const DavException( + kind: DavErrorKind.tls, + code: 'DavTlsFailure', + safeMessage: 'The Nextcloud TLS connection could not be verified.', + ); + } on Object { + throw const DavException( + kind: DavErrorKind.network, + code: 'DavTransientNetwork', + safeMessage: 'The Nextcloud login server could not be reached.', + ); + } + final bytes = await _readBody(streamed, cancellation); + if (!_isRedirect(streamed.statusCode)) { + return _FlowResponse( + statusCode: streamed.statusCode, + bodyBytes: bytes, + requestUri: current, + ); + } + final location = streamed.headers['location']; + if (location == null || redirects == maximumRedirects) { + throw _redirectError('NextcloudLoginRedirectLimitExceeded'); + } + final destination = current.resolve(location); + if (!_safeHttps(destination) || + !_withinInstallationPath(destination.path, trustedBase.path) || + !_sameOrigin(destination, trustedBase)) { + throw _redirectError('NextcloudLoginRedirectRejected'); + } + current = destination; + } + throw StateError('Unreachable Nextcloud redirect state.'); + } + + Future _readBody( + http.StreamedResponse response, + DavCancellationToken cancellation, + ) async { + final bytes = BytesBuilder(copy: false); + var length = 0; + try { + await for (final chunk in response.stream.timeout(responseTimeout)) { + cancellation.throwIfCancelled(); + length += chunk.length; + if (length > maximumResponseBytes) { + throw const DavException( + kind: DavErrorKind.responseTooLarge, + code: 'NextcloudLoginResponseTooLarge', + safeMessage: 'The Nextcloud login response was too large.', + ); + } + bytes.add(chunk); + } + return bytes.takeBytes(); + } on TimeoutException { + throw const DavException( + kind: DavErrorKind.timeout, + code: 'NextcloudLoginResponseTimeout', + safeMessage: 'The Nextcloud login server did not respond in time.', + ); + } + } + + void _clear(Future operation) { + if (identical(_activeOperation, operation)) { + _activeOperation = null; + _activeCancellation = null; + } + } +} + +Uri normalizeNextcloudLoginServer(String enteredServer) { + var source = enteredServer.trim(); + if (source.isEmpty) { + throw const FormatException('Enter a Nextcloud server address.'); + } + if (!source.contains('://')) source = 'https://$source'; + final parsed = Uri.tryParse(source); + if (parsed == null || + (parsed.scheme != 'http' && parsed.scheme != 'https') || + parsed.host.isEmpty || + parsed.userInfo.isNotEmpty || + parsed.hasQuery || + parsed.hasFragment) { + throw const FormatException('Enter a valid Nextcloud HTTPS server.'); + } + var path = parsed.normalizePath().path; + while (path.length > 1 && path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + path = _nextcloudInstallationPath(path); + return Uri( + scheme: 'https', + host: parsed.host.toLowerCase(), + port: parsed.hasPort && parsed.port != 443 ? parsed.port : null, + path: path == '/' ? '' : path, + ); +} + +String _nextcloudInstallationPath(String enteredPath) { + final normalized = enteredPath.toLowerCase(); + const davEndpoints = [ + '/remote.php/dav', + '/remote.php/caldav', + '/remote.php/webdav', + ]; + for (final endpoint in davEndpoints) { + final index = normalized.indexOf(endpoint); + if (index < 0) continue; + final endpointEnd = index + endpoint.length; + if (endpointEnd == normalized.length || + normalized.codeUnitAt(endpointEnd) == 0x2f) { + return enteredPath.substring(0, index); + } + } + return enteredPath; +} + +final class _FlowResponse { + const _FlowResponse({ + required this.statusCode, + required this.bodyBytes, + required this.requestUri, + }); + + final int statusCode; + final Uint8List bodyBytes; + final Uri requestUri; +} + +Map _jsonObject(Uint8List bytes) { + try { + final decoded = jsonDecode(utf8.decode(bytes, allowMalformed: false)); + if (decoded is! Map) throw const FormatException(); + return decoded.cast(); + } on Object { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'NextcloudLoginMalformedResponse', + safeMessage: 'Nextcloud returned a malformed login response.', + ); + } +} + +Map _jsonObjectValue(Map json, String key) { + final value = json[key]; + if (value is! Map) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'NextcloudLoginMalformedResponse', + safeMessage: 'Nextcloud returned a malformed login response.', + ); + } + return value.cast(); +} + +String _requiredJsonString(Map json, String key) { + final value = json[key]; + if (value is! String || value.trim().isEmpty || value.length > 16384) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'NextcloudLoginMalformedResponse', + safeMessage: 'Nextcloud returned a malformed login response.', + ); + } + return value; +} + +Uri _validatedFlowUri( + String source, { + required Uri responseUri, + required String installationPath, + required bool allowQuery, +}) { + final uri = Uri.tryParse(source); + if (uri == null || + !_safeHttps(uri) || + !_sameOrigin(uri, responseUri) || + !_withinInstallationPath(uri.path, installationPath) || + (!allowQuery && uri.hasQuery)) { + throw _redirectError('NextcloudLoginEndpointRejected'); + } + return uri; +} + +Uri _appendPath(Uri base, String suffix) { + final path = base.path.isEmpty + ? '/$suffix' + : '${base.path.endsWith('/') ? base.path : '${base.path}/'}$suffix'; + return base.replace(path: path); +} + +String _formEncode(Map values) => values.entries + .map( + (entry) => + '${Uri.encodeQueryComponent(entry.key)}=' + '${Uri.encodeQueryComponent(entry.value)}', + ) + .join('&'); + +bool _safeHttps(Uri uri) => + uri.scheme.toLowerCase() == 'https' && + uri.host.isNotEmpty && + uri.userInfo.isEmpty && + !uri.hasFragment; + +bool _sameOrigin(Uri left, Uri right) => + left.scheme.toLowerCase() == right.scheme.toLowerCase() && + left.host.toLowerCase() == right.host.toLowerCase() && + left.port == right.port; + +bool _withinInstallationPath(String candidate, String installationPath) { + var base = installationPath; + while (base.length > 1 && base.endsWith('/')) { + base = base.substring(0, base.length - 1); + } + if (base.isEmpty || base == '/') return candidate.startsWith('/'); + return candidate == base || candidate.startsWith('$base/'); +} + +bool _isRedirect(int status) => + status == HttpStatus.movedPermanently || + status == HttpStatus.found || + status == HttpStatus.temporaryRedirect || + status == HttpStatus.permanentRedirect; + +DavException _redirectError(String code) => DavException( + kind: DavErrorKind.redirectRejected, + code: code, + safeMessage: 'Nextcloud returned an untrusted login destination.', +); + +Future _launchExternalBrowser(Uri uri) => + launchUrl(uri, mode: LaunchMode.externalApplication); diff --git a/lib/src/dav/dav_errors.dart b/lib/src/dav/dav_errors.dart new file mode 100644 index 0000000..cf75aa7 --- /dev/null +++ b/lib/src/dav/dav_errors.dart @@ -0,0 +1,293 @@ +import 'dart:io'; + +enum DavErrorKind { + cancelled, + timeout, + network, + tls, + authentication, + authorization, + redirectRejected, + redirectLoop, + responseTooLarge, + malformedXml, + malformedStatus, + protocol, + rateLimited, + invalidSyncToken, + preconditionFailed, + uidConflict, + invalidCalendarData, + unsupportedComponent, + maximumResourceSize, + limitExceeded, + notFound, + conflict, + server, +} + +/// DAV failure categories used by retry policy and user-facing error handling. +enum DavErrorCategory { + davAuthRejected, + davCredentialsRevoked, + davTlsFailure, + davDiscoveryFailed, + davUnsupportedServer, + davPermissionDenied, + davResourceConflict, + davUidConflict, + davMalformedResource, + davUnsupportedComponent, + davQuotaOrSizeLimit, + davRateLimited, + davTransientNetwork, + davServerUnavailable, + davSyncTokenInvalid, + davCollectionRemoved, + davReadOnly, + davProtocolViolation, + davOperationCancelled, +} + +final class DavErrorDisposition { + const DavErrorDisposition({ + required this.category, + required this.retryable, + required this.requiresUserAction, + required this.cachedDataUsable, + }); + + final DavErrorCategory category; + final bool retryable; + final bool requiresUserAction; + final bool cachedDataUsable; +} + +/// A typed DAV failure whose diagnostic representation excludes request URLs, +/// response bodies, credentials, and user calendar content. +final class DavException implements Exception { + const DavException({ + required this.kind, + required this.code, + required this.safeMessage, + this.statusCode, + this.correlationId, + this.retryAfter, + this.categoryOverride, + }); + + final DavErrorKind kind; + final String code; + final String safeMessage; + final int? statusCode; + final String? correlationId; + final Duration? retryAfter; + final DavErrorCategory? categoryOverride; + + DavErrorDisposition get disposition => classifyDavError( + kind: kind, + code: code, + statusCode: statusCode, + categoryOverride: categoryOverride, + ); + + DavErrorCategory get category => disposition.category; + bool get retryable => disposition.retryable; + bool get requiresUserAction => disposition.requiresUserAction; + bool get cachedDataUsable => disposition.cachedDataUsable; + + @override + String toString() => + 'DavException(kind: ${kind.name}, code: $code, ' + 'statusCode: $statusCode, correlationId: $correlationId)'; +} + +DavErrorDisposition classifyDavError({ + required DavErrorKind kind, + required String code, + int? statusCode, + DavErrorCategory? categoryOverride, +}) { + final category = + categoryOverride ?? + _categoryFor(kind: kind, code: code, statusCode: statusCode); + final retryable = switch (category) { + DavErrorCategory.davRateLimited || + DavErrorCategory.davTransientNetwork || + DavErrorCategory.davServerUnavailable => true, + DavErrorCategory.davDiscoveryFailed => + kind == DavErrorKind.timeout || + kind == DavErrorKind.network || + kind == DavErrorKind.server || + kind == DavErrorKind.rateLimited, + _ => false, + }; + final requiresUserAction = switch (category) { + DavErrorCategory.davAuthRejected || + DavErrorCategory.davCredentialsRevoked || + DavErrorCategory.davTlsFailure || + DavErrorCategory.davUnsupportedServer || + DavErrorCategory.davPermissionDenied || + DavErrorCategory.davResourceConflict || + DavErrorCategory.davUidConflict || + DavErrorCategory.davReadOnly => true, + DavErrorCategory.davDiscoveryFailed => !retryable, + _ => false, + }; + final cachedDataUsable = switch (category) { + DavErrorCategory.davMalformedResource || + DavErrorCategory.davUnsupportedComponent || + DavErrorCategory.davCollectionRemoved => false, + _ => true, + }; + return DavErrorDisposition( + category: category, + retryable: retryable, + requiresUserAction: requiresUserAction, + cachedDataUsable: cachedDataUsable, + ); +} + +DavErrorCategory _categoryFor({ + required DavErrorKind kind, + required String code, + required int? statusCode, +}) { + final normalizedCode = code.toLowerCase(); + if (normalizedCode.contains('credentialsrevoked')) { + return DavErrorCategory.davCredentialsRevoked; + } + if (normalizedCode.contains('authrejected')) { + return DavErrorCategory.davAuthRejected; + } + if (normalizedCode.contains('unsupportedserver') || + normalizedCode.contains('unsupportedprofile')) { + return DavErrorCategory.davUnsupportedServer; + } + if (normalizedCode.contains('discoveryfailed')) { + return DavErrorCategory.davDiscoveryFailed; + } + if (normalizedCode.contains('permissiondenied')) { + return DavErrorCategory.davPermissionDenied; + } + if (normalizedCode.contains('readonly')) { + return DavErrorCategory.davReadOnly; + } + if (normalizedCode.contains('uidconflict')) { + return DavErrorCategory.davUidConflict; + } + if (normalizedCode.contains('resourceconflict') || + normalizedCode.contains('resourcelocked') || + normalizedCode.startsWith('davconflict')) { + return DavErrorCategory.davResourceConflict; + } + if (normalizedCode.contains('malformedresource')) { + return DavErrorCategory.davMalformedResource; + } + if (normalizedCode.contains('unsupportedcomponent')) { + return DavErrorCategory.davUnsupportedComponent; + } + if (normalizedCode.contains('quotaorsizelimit') || + normalizedCode.contains('maximumresourcesize')) { + return DavErrorCategory.davQuotaOrSizeLimit; + } + if (normalizedCode.contains('ratelimited')) { + return DavErrorCategory.davRateLimited; + } + if (normalizedCode.contains('tlsfailure')) { + return DavErrorCategory.davTlsFailure; + } + if (normalizedCode.contains('networkfailure') || + normalizedCode.contains('timeout')) { + return DavErrorCategory.davTransientNetwork; + } + if (normalizedCode.contains('serverunavailable')) { + return DavErrorCategory.davServerUnavailable; + } + if (normalizedCode.contains('synctokeninvalid')) { + return DavErrorCategory.davSyncTokenInvalid; + } + if (normalizedCode.contains('collectionremoved') || + normalizedCode.contains('collectionnotfound')) { + return DavErrorCategory.davCollectionRemoved; + } + + if (statusCode != null) { + if (statusCode == HttpStatus.unauthorized) { + return DavErrorCategory.davAuthRejected; + } + if (statusCode == HttpStatus.forbidden) { + return DavErrorCategory.davPermissionDenied; + } + if (statusCode == HttpStatus.notFound || statusCode == HttpStatus.gone) { + return DavErrorCategory.davCollectionRemoved; + } + if (statusCode == HttpStatus.conflict || + statusCode == HttpStatus.preconditionFailed || + statusCode == HttpStatus.locked) { + return DavErrorCategory.davResourceConflict; + } + if (statusCode == HttpStatus.tooManyRequests) { + return DavErrorCategory.davRateLimited; + } + if (statusCode == HttpStatus.insufficientStorage) { + return DavErrorCategory.davQuotaOrSizeLimit; + } + if (statusCode >= 500) { + return DavErrorCategory.davServerUnavailable; + } + } + + return switch (kind) { + DavErrorKind.cancelled => DavErrorCategory.davOperationCancelled, + DavErrorKind.timeout || + DavErrorKind.network => DavErrorCategory.davTransientNetwork, + DavErrorKind.tls => DavErrorCategory.davTlsFailure, + DavErrorKind.authentication => DavErrorCategory.davAuthRejected, + DavErrorKind.authorization => DavErrorCategory.davPermissionDenied, + DavErrorKind.rateLimited => DavErrorCategory.davRateLimited, + DavErrorKind.invalidSyncToken => DavErrorCategory.davSyncTokenInvalid, + DavErrorKind.preconditionFailed || + DavErrorKind.conflict => DavErrorCategory.davResourceConflict, + DavErrorKind.uidConflict => DavErrorCategory.davUidConflict, + DavErrorKind.invalidCalendarData => DavErrorCategory.davMalformedResource, + DavErrorKind.unsupportedComponent => + DavErrorCategory.davUnsupportedComponent, + DavErrorKind.maximumResourceSize || + DavErrorKind.limitExceeded || + DavErrorKind.responseTooLarge => DavErrorCategory.davQuotaOrSizeLimit, + DavErrorKind.notFound => DavErrorCategory.davCollectionRemoved, + DavErrorKind.server => DavErrorCategory.davServerUnavailable, + DavErrorKind.redirectRejected || + DavErrorKind.redirectLoop || + DavErrorKind.malformedXml || + DavErrorKind.malformedStatus || + DavErrorKind.protocol => DavErrorCategory.davProtocolViolation, + }; +} + +/// Parses a server retry hint without including the original header in an +/// exception or log message. +Duration? parseDavRetryAfter( + String? value, { + DateTime? nowUtc, + Duration maximum = const Duration(hours: 24), +}) { + final trimmed = value?.trim(); + if (trimmed == null || trimmed.isEmpty) return null; + Duration? result; + final seconds = int.tryParse(trimmed); + if (seconds != null) { + if (seconds < 0) return null; + result = Duration(seconds: seconds); + } else { + try { + final deadline = HttpDate.parse(trimmed).toUtc(); + result = deadline.difference((nowUtc ?? DateTime.now().toUtc()).toUtc()); + if (result.isNegative) result = Duration.zero; + } on Object { + return null; + } + } + return result > maximum ? maximum : result; +} diff --git a/lib/src/dav/dav_href.dart b/lib/src/dav/dav_href.dart new file mode 100644 index 0000000..320777b --- /dev/null +++ b/lib/src/dav/dav_href.dart @@ -0,0 +1,54 @@ +import '../providers/busy_provider.dart'; +import 'dav_errors.dart'; +import 'dav_provider_profile.dart'; + +Uri resolveDavHref({ + required String href, + required Uri responseRequestUri, + required DavProviderProfile profile, + required Uri accountAuthority, + String? correlationId, +}) { + final source = href.trim(); + if (source.isEmpty) { + throw _invalidHref(correlationId); + } + late final Uri resolved; + try { + resolved = responseRequestUri.resolve(source); + } on FormatException { + throw _invalidHref(correlationId); + } + if (resolved.userInfo.isNotEmpty || + resolved.hasFragment || + resolved.hasQuery || + !profile.isTrustedCredentialDestination( + resolved, + accountAuthority: accountAuthority, + )) { + throw _invalidHref(correlationId); + } + return resolved; +} + +/// Returns the stable account-relative DAV identity without decoding or +/// re-encoding percent-escaped reserved path octets. iCloud shard hosts are +/// intentionally excluded; Nextcloud authority is already part of account +/// identity. +String normalizedDavHrefKey(BusyProvider provider, Uri requestUri) { + if (requestUri.path.isEmpty || !requestUri.path.startsWith('/')) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavHrefMissingAbsolutePath', + safeMessage: 'A DAV resource did not have an absolute path.', + ); + } + return requestUri.path; +} + +DavException _invalidHref(String? correlationId) => DavException( + kind: DavErrorKind.redirectRejected, + code: 'DavHrefDestinationRejected', + safeMessage: 'The DAV server returned an unsafe resource location.', + correlationId: correlationId, +); diff --git a/lib/src/dav/dav_provider_profile.dart b/lib/src/dav/dav_provider_profile.dart new file mode 100644 index 0000000..1a69ddb --- /dev/null +++ b/lib/src/dav/dav_provider_profile.dart @@ -0,0 +1,132 @@ +import '../providers/busy_provider.dart'; + +const davProviderProfileVersion = 2; + +final class DavProviderProfile { + const DavProviderProfile({ + required this.provider, + required this.bootstrapUri, + required this.calendarEnabled, + required this.tasksEnabled, + required this.allowCollectionMutations, + required this.allowSchedulingMutations, + required this.allowMove, + this.allowInsecureLoopbackForTesting = false, + }); + + final BusyProvider provider; + final Uri bootstrapUri; + final bool calendarEnabled; + final bool tasksEnabled; + final bool allowCollectionMutations; + final bool allowSchedulingMutations; + final bool allowMove; + final bool allowInsecureLoopbackForTesting; + + bool isTrustedCredentialDestination( + Uri destination, { + required Uri accountAuthority, + }) { + if (!_isSafeHttpsUri(destination) && + !(allowInsecureLoopbackForTesting && + _isInsecureLoopbackUri(destination))) { + return false; + } + return switch (provider) { + BusyProvider.appleICloud => _isApprovedICloudHost(destination.host), + BusyProvider.nextcloud => + _sameOrigin(destination, accountAuthority) && + _preservesInstallationPath(destination, accountAuthority), + BusyProvider.google || BusyProvider.microsoft => false, + }; + } +} + +bool _isInsecureLoopbackUri(Uri uri) => + uri.scheme.toLowerCase() == 'http' && + (uri.host == '127.0.0.1' || uri.host == '::1' || uri.host == 'localhost') && + uri.userInfo.isEmpty && + !uri.hasFragment; + +DavProviderProfile davProviderProfile( + BusyProvider provider, { + Uri? nextcloudServer, +}) => switch (provider) { + BusyProvider.appleICloud => DavProviderProfile( + provider: provider, + bootstrapUri: Uri.parse('https://caldav.icloud.com/'), + calendarEnabled: true, + tasksEnabled: false, + allowCollectionMutations: false, + allowSchedulingMutations: false, + allowMove: false, + ), + BusyProvider.nextcloud => DavProviderProfile( + provider: provider, + bootstrapUri: + nextcloudServer ?? + (throw ArgumentError.value( + nextcloudServer, + 'nextcloudServer', + 'Nextcloud requires the canonical Login Flow v2 server.', + )), + calendarEnabled: true, + tasksEnabled: true, + allowCollectionMutations: true, + allowSchedulingMutations: false, + allowMove: true, + ), + BusyProvider.google || BusyProvider.microsoft => throw ArgumentError.value( + provider, + 'provider', + 'The provider does not use the DAV transport.', + ), +}; + +Uri davWellKnownUri(DavProviderProfile profile) { + final bootstrap = profile.bootstrapUri; + if (profile.provider == BusyProvider.appleICloud) { + return bootstrap.replace( + path: '/.well-known/caldav', + query: null, + fragment: null, + ); + } + final basePath = bootstrap.path.endsWith('/') + ? bootstrap.path + : '${bootstrap.path}/'; + return bootstrap.replace( + path: '$basePath.well-known/caldav', + query: null, + fragment: null, + ); +} + +bool _isSafeHttpsUri(Uri uri) => + uri.scheme.toLowerCase() == 'https' && + uri.host.isNotEmpty && + uri.userInfo.isEmpty && + !uri.hasFragment; + +bool _sameOrigin(Uri left, Uri right) => + left.scheme.toLowerCase() == right.scheme.toLowerCase() && + left.host.toLowerCase() == right.host.toLowerCase() && + left.port == right.port; + +bool _preservesInstallationPath(Uri destination, Uri authority) { + var basePath = authority.path; + if (basePath.isEmpty || basePath == '/') { + return true; + } + while (basePath.endsWith('/')) { + basePath = basePath.substring(0, basePath.length - 1); + } + return destination.path == basePath || + destination.path.startsWith('$basePath/'); +} + +bool _isApprovedICloudHost(String value) { + final host = value.toLowerCase(); + return host == 'caldav.icloud.com' || + RegExp(r'^p[0-9]+-caldav[.]icloud[.]com$').hasMatch(host); +} diff --git a/lib/src/dav/discovery/dav_discovery_models.dart b/lib/src/dav/discovery/dav_discovery_models.dart new file mode 100644 index 0000000..a068089 --- /dev/null +++ b/lib/src/dav/discovery/dav_discovery_models.dart @@ -0,0 +1,118 @@ +import '../../providers/busy_provider.dart'; +import '../../providers/provider_capabilities.dart'; + +const davCapabilitiesSchemaVersion = 1; +const davComponentEvent = 1 << 0; +const davComponentTodo = 1 << 1; +const davComponentTimezone = 1 << 2; +const davComponentJournal = 1 << 3; +const davComponentFreeBusy = 1 << 4; + +enum DavCollectionKind { + writableEventCalendar, + readOnlyEventCalendar, + writableTaskList, + readOnlyTaskList, + mixedCalendar, + subscribedCalendar, + schedulingInbox, + schedulingOutbox, + notifications, + unsupported, +} + +final class DavServiceDiscovery { + const DavServiceDiscovery({ + required this.canonicalServiceUri, + required this.canonicalOrigin, + required this.principalHref, + required this.calendarHomeHref, + required this.calendarUserAddresses, + required this.scheduleInboxHref, + required this.scheduleOutboxHref, + required this.capabilities, + required this.discoveredAtUtc, + required this.lastValidatedAtUtc, + required this.providerProfileVersion, + }); + + final Uri canonicalServiceUri; + final Uri canonicalOrigin; + final Uri principalHref; + final Uri calendarHomeHref; + final List calendarUserAddresses; + final Uri? scheduleInboxHref; + final Uri? scheduleOutboxHref; + final AccountServiceCapabilities capabilities; + final DateTime discoveredAtUtc; + final DateTime lastValidatedAtUtc; + final int providerProfileVersion; +} + +final class DavCollectionDiscovery { + const DavCollectionDiscovery({ + required this.hrefKey, + required this.requestUri, + required this.displayName, + required this.description, + required this.resourceTypes, + required this.supportedComponentMask, + required this.supportedCalendarData, + required this.supportedReports, + required this.currentUserPrivileges, + required this.ownerHref, + required this.safeDisplayMetadata, + required this.color, + required this.sortOrder, + required this.calendarTimeZone, + required this.calendarTimeZoneId, + required this.scheduleTransparency, + required this.maximumResourceSize, + required this.maximumInstances, + required this.syncToken, + required this.ctag, + required this.capabilities, + required this.kind, + required this.eventProjectionEnabled, + required this.taskProjectionEnabled, + }); + + final String hrefKey; + final Uri requestUri; + final String displayName; + final String? description; + final Set resourceTypes; + final int supportedComponentMask; + final List> supportedCalendarData; + final Set supportedReports; + final Set currentUserPrivileges; + final String? ownerHref; + final Map safeDisplayMetadata; + final String? color; + final int? sortOrder; + final String? calendarTimeZone; + final String? calendarTimeZoneId; + final String? scheduleTransparency; + final int? maximumResourceSize; + final int? maximumInstances; + final String? syncToken; + final String? ctag; + final CollectionCapabilities capabilities; + final DavCollectionKind kind; + final bool eventProjectionEnabled; + final bool taskProjectionEnabled; +} + +final class DavDiscoveryResult { + const DavDiscoveryResult({ + required this.accountId, + required this.provider, + required this.service, + required this.collections, + }); + + final String accountId; + final BusyProvider provider; + final DavServiceDiscovery service; + final List collections; +} diff --git a/lib/src/dav/discovery/dav_discovery_repository.dart b/lib/src/dav/discovery/dav_discovery_repository.dart new file mode 100644 index 0000000..1686a4d --- /dev/null +++ b/lib/src/dav/discovery/dav_discovery_repository.dart @@ -0,0 +1,314 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../db/app_database.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_provider_profile.dart'; +import 'dav_discovery_models.dart'; + +final class DavDiscoveryRepository { + DavDiscoveryRepository({ + required AppDatabase database, + String Function()? idFactory, + }) : _database = database, + _idFactory = idFactory ?? const Uuid().v4; + + final AppDatabase _database; + final String Function() _idFactory; + + Future commitSuccessfulInventory(DavDiscoveryResult result) { + return _database.transaction(() async { + final service = result.service; + await _database + .into(_database.davAccountServices) + .insertOnConflictUpdate( + DavAccountServicesCompanion.insert( + accountId: result.accountId, + canonicalServiceUri: service.canonicalServiceUri.toString(), + canonicalOrigin: service.canonicalOrigin.toString(), + principalHref: Value(service.principalHref.toString()), + calendarHomeHref: Value(service.calendarHomeHref.toString()), + calendarUserAddressesJson: Value( + jsonEncode( + service.calendarUserAddresses + .map((address) => address.toString()) + .toList(growable: false), + ), + ), + scheduleInboxHref: Value(service.scheduleInboxHref?.toString()), + scheduleOutboxHref: Value(service.scheduleOutboxHref?.toString()), + capabilitiesJson: Value( + jsonEncode({ + 'hasPrincipal': service.capabilities.hasPrincipal, + 'hasCalendarHome': service.capabilities.hasCalendarHome, + 'hasSchedulingInbox': service.capabilities.hasSchedulingInbox, + 'hasSchedulingOutbox': + service.capabilities.hasSchedulingOutbox, + 'supportedReports': + service.capabilities.supportedReports.toList()..sort(), + 'serverFeatures': service.capabilities.serverFeatures.toList() + ..sort(), + }), + ), + capabilitiesSchemaVersion: const Value( + davCapabilitiesSchemaVersion, + ), + providerProfileVersion: Value(service.providerProfileVersion), + discoveredAtUtc: service.discoveredAtUtc.toIso8601String(), + lastValidatedAtUtc: Value( + service.lastValidatedAtUtc.toIso8601String(), + ), + lastDiscoveryErrorCode: const Value(null), + ), + ); + + final returnedIds = []; + for (final discovered in result.collections) { + final existing = + await (_database.select(_database.davCollections)..where( + (row) => + row.accountId.equals(result.accountId) & + row.hrefKey.equals(discovered.hrefKey), + )) + .getSingleOrNull(); + final id = existing?.id ?? _idFactory(); + returnedIds.add(id); + final now = service.discoveredAtUtc.toIso8601String(); + await _database + .into(_database.davCollections) + .insertOnConflictUpdate( + DavCollectionsCompanion.insert( + id: id, + accountId: result.accountId, + hrefKey: discovered.hrefKey, + requestUri: discovered.requestUri.toString(), + displayName: discovered.displayName, + description: Value(discovered.description), + resourceTypesJson: Value( + jsonEncode(discovered.resourceTypes.toList()..sort()), + ), + supportedComponentMask: Value( + discovered.supportedComponentMask, + ), + supportedCalendarDataJson: Value( + jsonEncode(discovered.supportedCalendarData), + ), + supportedReportsJson: Value( + jsonEncode(discovered.supportedReports.toList()..sort()), + ), + currentUserPrivilegesJson: Value( + jsonEncode(discovered.currentUserPrivileges.toList()..sort()), + ), + ownerHref: Value(discovered.ownerHref), + safeDisplayMetadataJson: Value( + discovered.safeDisplayMetadata.isEmpty + ? null + : jsonEncode(discovered.safeDisplayMetadata), + ), + color: Value(discovered.color), + sortOrder: Value(discovered.sortOrder), + calendarTimeZone: Value(discovered.calendarTimeZone), + calendarTimeZoneId: Value(discovered.calendarTimeZoneId), + scheduleTransparency: Value(discovered.scheduleTransparency), + maximumResourceSize: Value(discovered.maximumResourceSize), + maximumInstances: Value(discovered.maximumInstances), + syncToken: Value(discovered.syncToken), + ctag: Value(discovered.ctag), + readOnly: Value(discovered.capabilities.isReadOnly), + eventProjectionEnabled: Value( + discovered.eventProjectionEnabled, + ), + taskProjectionEnabled: Value(discovered.taskProjectionEnabled), + eventsSelected: Value(existing?.eventsSelected ?? true), + tasksSelected: Value(existing?.tasksSelected ?? true), + serverMissing: const Value(false), + deleted: const Value(false), + lastInventoryAtUtc: Value(now), + lastSyncAtUtc: Value(existing?.lastSyncAtUtc), + parserVersion: Value(existing?.parserVersion ?? 1), + projectionVersion: Value(existing?.projectionVersion ?? 1), + createdAtUtc: existing?.createdAtUtc ?? now, + updatedAtUtc: now, + ), + ); + await _upsertProjections( + result: result, + collectionId: id, + collection: discovered, + now: service.discoveredAtUtc, + existing: existing, + ); + } + + final missingQuery = _database.update(_database.davCollections) + ..where((row) { + final sameAccount = row.accountId.equals(result.accountId); + return returnedIds.isEmpty + ? sameAccount + : sameAccount & row.id.isNotIn(returnedIds); + }); + await missingQuery.write( + DavCollectionsCompanion( + serverMissing: const Value(true), + updatedAtUtc: Value(service.discoveredAtUtc.toIso8601String()), + ), + ); + await _markMissingProjections(result.accountId, returnedIds); + await (_database.update( + _database.accounts, + )..where((row) => row.id.equals(result.accountId))).write( + AccountsCompanion( + providerProfileVersion: const Value(davProviderProfileVersion), + updatedAtUtc: Value(service.discoveredAtUtc.toIso8601String()), + ), + ); + }); + } + + Future recordDiscoveryFailure(String accountId, String code) async { + final existing = await (_database.select( + _database.davAccountServices, + )..where((row) => row.accountId.equals(accountId))).getSingleOrNull(); + if (existing == null) return; + await (_database.update( + _database.davAccountServices, + )..where((row) => row.accountId.equals(accountId))).write( + DavAccountServicesCompanion(lastDiscoveryErrorCode: Value(code)), + ); + } + + Future _upsertProjections({ + required DavDiscoveryResult result, + required String collectionId, + required DavCollectionDiscovery collection, + required DateTime now, + required DavCollection? existing, + }) async { + final nowText = now.toUtc().toIso8601String(); + final nowEpoch = now.toUtc().millisecondsSinceEpoch; + final calendarSourceId = 'dav-calendar-$collectionId'; + if (collection.eventProjectionEnabled) { + await _database + .into(_database.calendarSources) + .insertOnConflictUpdate( + CalendarSourcesCompanion.insert( + id: calendarSourceId, + accountId: result.accountId, + provider: result.provider.storageValue, + providerCalendarId: collection.hrefKey, + davCollectionId: Value(collectionId), + summary: collection.displayName, + description: Value(collection.description), + selected: Value(existing?.eventsSelected ?? true), + hidden: const Value(false), + readOnly: Value(collection.capabilities.isReadOnly), + backgroundColor: Value(collection.color), + timeZone: Value(collection.calendarTimeZoneId), + accessRole: Value( + collection.capabilities.isReadOnly ? 'reader' : 'writer', + ), + isDeleted: const Value(false), + rawJson: Value(_projectionMetadata(collection)), + createdAtLocal: nowEpoch, + updatedAtLocal: nowEpoch, + ), + ); + } else { + await (_database.update( + _database.calendarSources, + )..where((row) => row.davCollectionId.equals(collectionId))).write( + CalendarSourcesCompanion( + hidden: const Value(true), + isDeleted: const Value(true), + updatedAtLocal: Value(nowEpoch), + ), + ); + } + + final taskListId = 'dav-task-list-$collectionId'; + if (collection.taskProjectionEnabled) { + final shared = _differentPrincipal( + collection.ownerHref, + result.service.principalHref.toString(), + ); + await _database + .into(_database.taskLists) + .insertOnConflictUpdate( + TaskListsCompanion.insert( + accountId: result.accountId, + id: taskListId, + davCollectionId: Value(collectionId), + title: collection.displayName, + rawJson: _projectionMetadata(collection), + isOwner: Value(!shared), + isShared: Value(shared), + serverMissing: const Value(false), + createdLocalAtUtc: existing?.createdAtUtc ?? nowText, + updatedLocalAtUtc: nowText, + ), + ); + } else { + await (_database.update( + _database.taskLists, + )..where((row) => row.davCollectionId.equals(collectionId))).write( + TaskListsCompanion( + serverMissing: const Value(true), + updatedLocalAtUtc: Value(nowText), + ), + ); + } + } + + Future _markMissingProjections( + String accountId, + List returnedCollectionIds, + ) async { + Expression missingCollection(Expression column) => + returnedCollectionIds.isEmpty + ? column.isNotNull() + : column.isNotNull() & column.isNotIn(returnedCollectionIds); + await (_database.update(_database.calendarSources)..where( + (row) => + row.accountId.equals(accountId) & + missingCollection(row.davCollectionId), + )) + .write( + const CalendarSourcesCompanion( + hidden: Value(true), + isDeleted: Value(true), + ), + ); + await (_database.update(_database.taskLists)..where( + (row) => + row.accountId.equals(accountId) & + missingCollection(row.davCollectionId), + )) + .write(const TaskListsCompanion(serverMissing: Value(true))); + } +} + +String _projectionMetadata(DavCollectionDiscovery collection) => jsonEncode({ + 'transport': 'caldav', + 'hrefKey': collection.hrefKey, + 'kind': collection.kind.name, + 'supportedComponentMask': collection.supportedComponentMask, + 'readOnly': collection.capabilities.isReadOnly, +}); + +bool _differentPrincipal(String? ownerHref, String principalHref) { + String? path(String? value) { + final uri = Uri.tryParse(value ?? ''); + if (uri == null || uri.path.isEmpty) return null; + var result = uri.path; + while (result.length > 1 && result.endsWith('/')) { + result = result.substring(0, result.length - 1); + } + return result; + } + + final owner = path(ownerHref); + final principal = path(principalHref); + return owner != null && principal != null && owner != principal; +} diff --git a/lib/src/dav/discovery/dav_discovery_service.dart b/lib/src/dav/discovery/dav_discovery_service.dart new file mode 100644 index 0000000..1588c75 --- /dev/null +++ b/lib/src/dav/discovery/dav_discovery_service.dart @@ -0,0 +1,715 @@ +import 'package:xml/xml.dart'; + +import '../../providers/provider_capabilities.dart'; +import '../dav_errors.dart'; +import '../dav_href.dart'; +import '../dav_provider_profile.dart'; +import '../http/dav_http_transport.dart'; +import '../xml/dav_xml.dart'; +import 'dav_discovery_models.dart'; + +final class DavDiscoveryService { + DavDiscoveryService({ + required DavHttpTransport transport, + required DavProviderProfile profile, + required Uri accountAuthority, + required String accountId, + required DavBasicCredential credential, + DavXmlParser xmlParser = const DavXmlParser(), + DateTime Function()? nowUtc, + }) : _transport = transport, + _profile = profile, + _accountAuthority = accountAuthority, + _accountId = accountId, + _credential = credential, + _xmlParser = xmlParser, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final DavHttpTransport _transport; + final DavProviderProfile _profile; + final Uri _accountAuthority; + final String _accountId; + final DavBasicCredential _credential; + final DavXmlParser _xmlParser; + final DateTime Function() _nowUtc; + + Future discover({ + required String correlationId, + DavCancellationToken? cancellationToken, + }) async { + final options = await _transport.send( + DavRequest( + method: 'OPTIONS', + uri: davWellKnownUri(_profile), + accountId: _accountId, + correlationId: correlationId, + retryClass: DavRetryClass.safeRead, + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + _requireSuccessfulOrDav(options); + final serviceCapabilities = _serviceCapabilities(options); + + final principalResponse = await _propfind( + uri: options.requestUri, + depth: '0', + body: _currentPrincipalPropfind, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + final principalSet = _xmlParser.parseMultistatus( + principalResponse.bodyBytes, + correlationId: correlationId, + ); + final principalHref = _requiredHrefProperty( + principalSet, + davNamespace, + 'current-user-principal', + responseUri: principalResponse.requestUri, + correlationId: correlationId, + ); + + final homeResponse = await _propfind( + uri: principalHref, + depth: '0', + body: _principalPropertiesPropfind, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + final principalProperties = _xmlParser.parseMultistatus( + homeResponse.bodyBytes, + correlationId: correlationId, + ); + final calendarHome = _requiredHrefProperty( + principalProperties, + caldavNamespace, + 'calendar-home-set', + responseUri: homeResponse.requestUri, + correlationId: correlationId, + ); + final addresses = _calendarAddressListProperty( + principalProperties, + caldavNamespace, + 'calendar-user-address-set', + responseUri: homeResponse.requestUri, + correlationId: correlationId, + ); + final inbox = _optionalHrefProperty( + principalProperties, + caldavNamespace, + 'schedule-inbox-URL', + responseUri: homeResponse.requestUri, + correlationId: correlationId, + ); + final outbox = _optionalHrefProperty( + principalProperties, + caldavNamespace, + 'schedule-outbox-URL', + responseUri: homeResponse.requestUri, + correlationId: correlationId, + ); + + final inventoryResponse = await _propfind( + uri: calendarHome, + depth: '1', + body: _calendarHomeInventoryPropfind, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + final inventory = _xmlParser.parseMultistatus( + inventoryResponse.bodyBytes, + correlationId: correlationId, + ); + final collections = _parseCollections( + inventory, + responseUri: inventoryResponse.requestUri, + home: calendarHome, + inbox: inbox, + outbox: outbox, + correlationId: correlationId, + ); + final now = _nowUtc().toUtc(); + final canonicalService = principalResponse.requestUri; + return DavDiscoveryResult( + accountId: _accountId, + provider: _profile.provider, + service: DavServiceDiscovery( + canonicalServiceUri: canonicalService, + canonicalOrigin: canonicalService.replace( + path: '', + query: null, + fragment: null, + ), + principalHref: principalHref, + calendarHomeHref: calendarHome, + calendarUserAddresses: addresses, + scheduleInboxHref: inbox, + scheduleOutboxHref: outbox, + capabilities: AccountServiceCapabilities( + hasPrincipal: true, + hasCalendarHome: true, + hasSchedulingInbox: inbox != null, + hasSchedulingOutbox: outbox != null, + supportedReports: serviceCapabilities.supportedReports, + serverFeatures: serviceCapabilities.serverFeatures, + ), + discoveredAtUtc: now, + lastValidatedAtUtc: now, + providerProfileVersion: davProviderProfileVersion, + ), + collections: List.unmodifiable(collections), + ); + } + + Future _propfind({ + required Uri uri, + required String depth, + required String body, + required String correlationId, + required DavCancellationToken? cancellationToken, + }) async { + final response = await _transport.send( + DavRequest.xml( + method: 'PROPFIND', + uri: uri, + accountId: _accountId, + correlationId: correlationId, + body: body, + headers: {'depth': depth}, + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + _requireMultistatus(response); + return response; + } + + AccountServiceCapabilities _serviceCapabilities(DavResponse response) { + final davTokens = (response.headers['dav'] ?? '') + .split(',') + .map((value) => value.trim()) + .where((value) => value.isNotEmpty) + .toSet(); + final allow = (response.headers['allow'] ?? '') + .split(',') + .map((value) => value.trim().toUpperCase()) + .where((value) => value.isNotEmpty) + .toSet(); + return AccountServiceCapabilities( + hasPrincipal: true, + hasCalendarHome: true, + supportedReports: {if (allow.contains('REPORT')) 'REPORT'}, + serverFeatures: {...davTokens, ...allow.map((method) => 'allow:$method')}, + ); + } + + List _parseCollections( + DavMultistatus inventory, { + required Uri responseUri, + required Uri home, + required Uri? inbox, + required Uri? outbox, + required String correlationId, + }) { + final result = []; + final homeKey = normalizedDavHrefKey(_profile.provider, home); + for (final response in inventory.responses) { + final responseStatus = response.statusCode; + if (response.isMissing || + (responseStatus != null && responseStatus >= 400)) { + continue; + } + final requestUri = resolveDavHref( + href: response.href, + responseRequestUri: responseUri, + profile: _profile, + accountAuthority: _accountAuthority, + correlationId: correlationId, + ); + final hrefKey = normalizedDavHrefKey(_profile.provider, requestUri); + if (hrefKey == homeKey) { + continue; + } + final resourceTypes = _nestedNames( + response.successfulProperty(davNamespace, 'resourcetype'), + ); + final isCalendar = resourceTypes.contains( + _name(caldavNamespace, 'calendar'), + ); + final isInbox = + _sameRequestTarget(requestUri, inbox) || + resourceTypes.contains(_name(caldavNamespace, 'schedule-inbox')); + final isOutbox = + _sameRequestTarget(requestUri, outbox) || + resourceTypes.contains(_name(caldavNamespace, 'schedule-outbox')); + final isSubscribed = resourceTypes.contains( + _name(calendarServerNamespace, 'subscribed'), + ); + if (!isCalendar && !isInbox && !isOutbox) { + continue; + } + + final componentProperty = response.successfulProperty( + caldavNamespace, + 'supported-calendar-component-set', + ); + final componentMask = _componentMask(componentProperty); + final supportsEvents = componentMask & davComponentEvent != 0; + final supportsTasks = componentMask & davComponentTodo != 0; + final reports = _reportNames( + response.successfulProperty(davNamespace, 'supported-report-set'), + ); + final privileges = _privilegeNames( + response.successfulProperty(davNamespace, 'current-user-privilege-set'), + ); + final hasAggregateAll = privileges.contains(_name(davNamespace, 'all')); + final hasAggregateWrite = + hasAggregateAll || privileges.contains(_name(davNamespace, 'write')); + final capabilities = CollectionCapabilities( + canRead: + hasAggregateWrite || + privileges.contains(_name(davNamespace, 'read')), + canReadPrivileges: + hasAggregateAll || + privileges.contains( + _name(davNamespace, 'read-current-user-privilege-set'), + ), + canWriteContent: + hasAggregateWrite || + privileges.contains(_name(davNamespace, 'write-content')), + canWriteProperties: + hasAggregateWrite || + privileges.contains(_name(davNamespace, 'write-properties')), + canAddMembers: + hasAggregateWrite || + privileges.contains(_name(davNamespace, 'bind')), + canDeleteMembers: + hasAggregateWrite || + privileges.contains(_name(davNamespace, 'unbind')), + canReadFreeBusy: + hasAggregateAll || + privileges.contains(_name(caldavNamespace, 'read-free-busy')), + supportsEvents: supportsEvents, + supportsTasks: supportsTasks, + supportsSyncCollection: reports.contains( + _name(davNamespace, 'sync-collection'), + ), + supportsCalendarMultiget: reports.contains( + _name(caldavNamespace, 'calendar-multiget'), + ), + supportsCalendarQuery: reports.contains( + _name(caldavNamespace, 'calendar-query'), + ), + supportedCalendarData: _calendarDataFormats( + response.successfulProperty( + caldavNamespace, + 'supported-calendar-data', + ), + ).map((entry) => entry['contentType'] ?? '').toSet(), + maximumResourceSize: _integerProperty( + response.successfulProperty(caldavNamespace, 'max-resource-size'), + ), + maximumInstances: _integerProperty( + response.successfulProperty(caldavNamespace, 'max-instances'), + ), + providerAllowsCollectionMutation: _profile.allowCollectionMutations, + providerAllowsSchedulingMutation: _profile.allowSchedulingMutations, + ); + final kind = _classify( + isInbox: isInbox, + isOutbox: isOutbox, + isSubscribed: isSubscribed, + hrefKey: hrefKey, + supportsEvents: supportsEvents, + supportsTasks: supportsTasks, + capabilities: capabilities, + ); + final calendarData = _calendarDataFormats( + response.successfulProperty(caldavNamespace, 'supported-calendar-data'), + ); + result.add( + DavCollectionDiscovery( + hrefKey: hrefKey, + requestUri: requestUri, + displayName: + _textProperty( + response.successfulProperty(davNamespace, 'displayname'), + ) ?? + _fallbackDisplayName(hrefKey), + description: _textProperty( + response.successfulProperty( + caldavNamespace, + 'calendar-description', + ), + ), + resourceTypes: resourceTypes, + supportedComponentMask: componentMask, + supportedCalendarData: calendarData, + supportedReports: reports, + currentUserPrivileges: privileges, + ownerHref: _rawHref( + response.successfulProperty(davNamespace, 'owner'), + ), + safeDisplayMetadata: _safeDisplayMetadata(response), + color: _textProperty( + response.successfulProperty(appleIcalNamespace, 'calendar-color'), + ), + sortOrder: _integerProperty( + response.successfulProperty(appleIcalNamespace, 'calendar-order'), + ), + calendarTimeZone: _textProperty( + response.successfulProperty(caldavNamespace, 'calendar-timezone'), + ), + calendarTimeZoneId: _textProperty( + response.successfulProperty( + caldavNamespace, + 'calendar-timezone-id', + ), + ), + scheduleTransparency: _nestedNames( + response.successfulProperty( + caldavNamespace, + 'schedule-calendar-transp', + ), + ).firstOrNull, + maximumResourceSize: capabilities.maximumResourceSize, + maximumInstances: capabilities.maximumInstances, + syncToken: _textProperty( + response.successfulProperty(davNamespace, 'sync-token'), + ), + ctag: _textProperty( + response.successfulProperty(calendarServerNamespace, 'getctag'), + ), + capabilities: capabilities, + kind: kind, + eventProjectionEnabled: + !isInbox && + !isOutbox && + _profile.calendarEnabled && + supportsEvents, + taskProjectionEnabled: + !isInbox && !isOutbox && _profile.tasksEnabled && supportsTasks, + ), + ); + } + return result; + } + + DavCollectionKind _classify({ + required bool isInbox, + required bool isOutbox, + required bool isSubscribed, + required String hrefKey, + required bool supportsEvents, + required bool supportsTasks, + required CollectionCapabilities capabilities, + }) { + if (isInbox) return DavCollectionKind.schedulingInbox; + if (isOutbox) return DavCollectionKind.schedulingOutbox; + if (isSubscribed) return DavCollectionKind.subscribedCalendar; + final lowered = hrefKey.toLowerCase(); + if (lowered.contains('/notifications/') || lowered.contains('/trashbin/')) { + return DavCollectionKind.notifications; + } + if (supportsEvents && supportsTasks) return DavCollectionKind.mixedCalendar; + if (supportsEvents) { + return capabilities.isReadOnly + ? DavCollectionKind.readOnlyEventCalendar + : DavCollectionKind.writableEventCalendar; + } + if (supportsTasks) { + return capabilities.isReadOnly + ? DavCollectionKind.readOnlyTaskList + : DavCollectionKind.writableTaskList; + } + return DavCollectionKind.unsupported; + } + + Uri _requiredHrefProperty( + DavMultistatus multistatus, + String namespaceUri, + String localName, { + required Uri responseUri, + required String correlationId, + }) { + final result = _optionalHrefProperty( + multistatus, + namespaceUri, + localName, + responseUri: responseUri, + correlationId: correlationId, + ); + if (result == null) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavUnsupportedServer', + safeMessage: 'The DAV server omitted a required discovery property.', + correlationId: correlationId, + categoryOverride: DavErrorCategory.davUnsupportedServer, + ); + } + return result; + } + + Uri? _optionalHrefProperty( + DavMultistatus multistatus, + String namespaceUri, + String localName, { + required Uri responseUri, + required String correlationId, + }) { + for (final response in multistatus.responses) { + final property = response.successfulProperty(namespaceUri, localName); + final href = _hrefChildren(property).firstOrNull; + if (href != null) { + return resolveDavHref( + href: href, + responseRequestUri: responseUri, + profile: _profile, + accountAuthority: _accountAuthority, + correlationId: correlationId, + ); + } + } + return null; + } + + List _calendarAddressListProperty( + DavMultistatus multistatus, + String namespaceUri, + String localName, { + required Uri responseUri, + required String correlationId, + }) { + final result = []; + for (final response in multistatus.responses) { + final property = response.successfulProperty(namespaceUri, localName); + for (final href in _hrefChildren(property)) { + final parsed = Uri.tryParse(href); + if (parsed != null && + (parsed.scheme.toLowerCase() == 'mailto' || + parsed.scheme.toLowerCase() == 'urn') && + !parsed.hasFragment) { + result.add(parsed); + } else { + result.add( + resolveDavHref( + href: href, + responseRequestUri: responseUri, + profile: _profile, + accountAuthority: _accountAuthority, + correlationId: correlationId, + ), + ); + } + } + } + return List.unmodifiable(result); + } + + void _requireSuccessfulOrDav(DavResponse response) { + if ((response.statusCode >= 200 && response.statusCode < 300) || + response.statusCode == 207) { + return; + } + throw _statusException(response); + } + + void _requireMultistatus(DavResponse response) { + if (response.statusCode != 207) { + throw _statusException(response); + } + } + + DavException _statusException(DavResponse response) { + final mapped = switch (response.statusCode) { + 401 => ( + DavErrorKind.authentication, + DavErrorCategory.davAuthRejected, + 'DavAuthRejected', + ), + 403 => ( + DavErrorKind.authorization, + DavErrorCategory.davPermissionDenied, + 'DavPermissionDenied', + ), + 404 => ( + DavErrorKind.protocol, + DavErrorCategory.davUnsupportedServer, + 'DavUnsupportedServer', + ), + 429 => ( + DavErrorKind.rateLimited, + DavErrorCategory.davRateLimited, + 'DavRateLimited', + ), + >= 500 => ( + DavErrorKind.server, + DavErrorCategory.davServerUnavailable, + 'DavServerUnavailable', + ), + _ => ( + DavErrorKind.protocol, + DavErrorCategory.davDiscoveryFailed, + 'DavDiscoveryFailed', + ), + }; + return DavException( + kind: mapped.$1, + code: mapped.$3, + safeMessage: 'The DAV server rejected the discovery request.', + statusCode: response.statusCode, + correlationId: response.correlationId, + retryAfter: parseDavRetryAfter(response.headers['retry-after']), + categoryOverride: mapped.$2, + ); + } +} + +String _name(String? namespace, String local) => '{$namespace}$local'; + +Set _nestedNames(DavProperty? property) => { + if (property != null) + for (final element in property.element.descendantElements) + _name(element.name.namespaceUri, element.name.local), +}; + +Set _reportNames(DavProperty? property) { + if (property == null) return const {}; + final result = {}; + for (final report in property.element.descendantElements.where( + (element) => + element.name.namespaceUri == davNamespace && + element.name.local == 'report', + )) { + final child = report.childElements.firstOrNull; + if (child != null) { + result.add(_name(child.name.namespaceUri, child.name.local)); + } + } + return result; +} + +Set _privilegeNames(DavProperty? property) { + if (property == null) return const {}; + final result = {}; + for (final privilege in property.element.descendantElements.where( + (element) => + element.name.namespaceUri == davNamespace && + element.name.local == 'privilege', + )) { + final child = privilege.childElements.firstOrNull; + if (child != null) { + result.add(_name(child.name.namespaceUri, child.name.local)); + } + } + return result; +} + +int _componentMask(DavProperty? property) { + if (property == null) { + return davComponentEvent | + davComponentTodo | + davComponentJournal | + davComponentFreeBusy; + } + var result = 0; + for (final component in property.element.descendantElements.where( + (element) => + element.name.namespaceUri == caldavNamespace && + element.name.local == 'comp', + )) { + result |= switch (component.getAttribute('name')?.toUpperCase()) { + 'VEVENT' => davComponentEvent, + 'VTODO' => davComponentTodo, + 'VTIMEZONE' => davComponentTimezone, + 'VJOURNAL' => davComponentJournal, + 'VFREEBUSY' => davComponentFreeBusy, + _ => 0, + }; + } + return result; +} + +List> _calendarDataFormats(DavProperty? property) { + if (property == null) return const []; + return [ + for (final element in property.element.descendantElements) + if (element.name.namespaceUri == caldavNamespace && + element.name.local == 'calendar-data') + { + if (element.getAttribute('content-type') case final value?) + 'contentType': value, + if (element.getAttribute('version') case final value?) + 'version': value, + }, + ]; +} + +String? _textProperty(DavProperty? property) { + final value = property?.text.trim(); + return value == null || value.isEmpty ? null : value; +} + +int? _integerProperty(DavProperty? property) => + int.tryParse(_textProperty(property) ?? ''); + +Iterable _hrefChildren(DavProperty? property) sync* { + if (property == null) return; + for (final element in property.element.descendantElements) { + if (element.name.namespaceUri == davNamespace && + element.name.local == 'href') { + final value = element.innerText.trim(); + if (value.isNotEmpty) yield value; + } + } +} + +String? _rawHref(DavProperty? property) => _hrefChildren(property).firstOrNull; + +bool _sameRequestTarget(Uri left, Uri? right) => + right != null && left.path == right.path; + +String _fallbackDisplayName(String hrefKey) => + hrefKey.split('/').where((part) => part.isNotEmpty).lastOrNull ?? + 'Calendar'; + +Map _safeDisplayMetadata(DavMultistatusResponse response) { + final result = {}; + for (final name in ['owner-display-name', 'calendar-enabled']) { + final value = _textProperty( + response.successfulProperty(nextcloudNamespace, name), + ); + if (value != null && value.length <= 512) { + result[name] = value; + } + } + return result; +} + +const _currentPrincipalPropfind = ''' +'''; + +const _principalPropertiesPropfind = ''' + + + + + +'''; + +const _calendarHomeInventoryPropfind = ''' + + + + + + + + + + +'''; diff --git a/lib/src/dav/http/dav_http_transport.dart b/lib/src/dav/http/dav_http_transport.dart new file mode 100644 index 0000000..bf9b16a --- /dev/null +++ b/lib/src/dav/http/dav_http_transport.dart @@ -0,0 +1,513 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; + +import '../../dav/dav_errors.dart'; +import '../dav_provider_profile.dart'; + +enum DavRetryClass { safeRead, conditionalMutation, never } + +final class DavCancellationToken { + bool _cancelled = false; + + bool get isCancelled => _cancelled; + + void cancel() => _cancelled = true; + + void throwIfCancelled({String? correlationId}) { + if (_cancelled) { + throw DavException( + kind: DavErrorKind.cancelled, + code: 'DavOperationCancelled', + safeMessage: 'The DAV operation was cancelled.', + correlationId: correlationId, + ); + } + } +} + +final class DavBasicCredential { + DavBasicCredential({required String username, required String password}) + : username = username.trim(), + password = password.trim() { + if (this.username.isEmpty || this.password.isEmpty) { + throw ArgumentError('DAV credentials must not be empty.'); + } + } + + final String username; + final String password; + + String get authorizationValue => + 'Basic ${base64Encode(utf8.encode('$username:$password'))}'; + + @override + String toString() => 'DavBasicCredential([REDACTED])'; +} + +final class DavRequest { + DavRequest({ + required this.method, + required this.uri, + required this.accountId, + required this.correlationId, + this.collectionId, + this.headers = const {}, + this.bodyBytes, + this.retryClass = DavRetryClass.never, + }) { + if (uri.userInfo.isNotEmpty) { + throw ArgumentError.value( + uri, + 'uri', + 'URI user information is forbidden.', + ); + } + if (headers.keys.any((name) => name.toLowerCase() == 'authorization')) { + throw ArgumentError( + 'Authorization is owned by DavHttpTransport and cannot be supplied.', + ); + } + } + + factory DavRequest.xml({ + required String method, + required Uri uri, + required String accountId, + required String correlationId, + required String body, + String? collectionId, + Map headers = const {}, + DavRetryClass retryClass = DavRetryClass.safeRead, + }) => DavRequest( + method: method, + uri: uri, + accountId: accountId, + correlationId: correlationId, + collectionId: collectionId, + headers: {'content-type': 'application/xml; charset=utf-8', ...headers}, + bodyBytes: Uint8List.fromList(utf8.encode(body)), + retryClass: retryClass, + ); + + factory DavRequest.icalendar({ + required String method, + required Uri uri, + required String accountId, + required String correlationId, + required String body, + required String collectionId, + required Map headers, + }) => DavRequest( + method: method, + uri: uri, + accountId: accountId, + correlationId: correlationId, + collectionId: collectionId, + headers: {'content-type': 'text/calendar; charset=utf-8', ...headers}, + bodyBytes: Uint8List.fromList(utf8.encode(body)), + retryClass: DavRetryClass.conditionalMutation, + ); + + final String method; + final Uri uri; + final String accountId; + final String correlationId; + final String? collectionId; + final Map headers; + final Uint8List? bodyBytes; + final DavRetryClass retryClass; +} + +final class DavResponse { + const DavResponse({ + required this.statusCode, + required this.headers, + required this.bodyBytes, + required this.requestUri, + required this.correlationId, + }); + + final int statusCode; + final Map headers; + final Uint8List bodyBytes; + final Uri requestUri; + final String correlationId; + + String get bodyText => utf8.decode(bodyBytes, allowMalformed: false); + + String? get etag => headers['etag']; +} + +final class DavTransportLimits { + const DavTransportLimits({ + this.connectTimeout = const Duration(seconds: 15), + this.responseTimeout = const Duration(seconds: 30), + this.operationTimeout = const Duration(minutes: 2), + this.maximumResponseBytes = 16 * 1024 * 1024, + this.maximumRedirects = 5, + this.maximumReadAttempts = 3, + this.maximumConcurrentPerAccount = 4, + this.maximumConcurrentPerCollection = 2, + }); + + final Duration connectTimeout; + final Duration responseTimeout; + final Duration operationTimeout; + final int maximumResponseBytes; + final int maximumRedirects; + final int maximumReadAttempts; + final int maximumConcurrentPerAccount; + final int maximumConcurrentPerCollection; +} + +typedef DavDelay = Future Function(Duration duration); + +final class DavHttpTransport { + DavHttpTransport({ + required http.Client client, + required DavProviderProfile profile, + required Uri accountAuthority, + DavTransportLimits limits = const DavTransportLimits(), + DavDelay? delay, + Random? random, + DateTime Function()? nowUtc, + }) : _client = client, + _profile = profile, + _accountAuthority = accountAuthority, + _limits = limits, + _delay = delay ?? Future.delayed, + _random = random ?? Random.secure(), + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final http.Client _client; + final DavProviderProfile _profile; + final Uri _accountAuthority; + final DavTransportLimits _limits; + final DavDelay _delay; + final Random _random; + final DateTime Function() _nowUtc; + final _accountSemaphores = {}; + final _collectionSemaphores = {}; + + Future send( + DavRequest request, { + required DavBasicCredential credential, + DavCancellationToken? cancellationToken, + }) { + final token = cancellationToken ?? DavCancellationToken(); + return _withConcurrencyLimit(request, () { + return _sendWithRetry(request, credential, token).timeout( + _limits.operationTimeout, + onTimeout: () => throw DavException( + kind: DavErrorKind.timeout, + code: 'DavOperationTimeout', + safeMessage: 'The DAV operation timed out.', + correlationId: request.correlationId, + ), + ); + }); + } + + Future _withConcurrencyLimit( + DavRequest request, + Future Function() action, + ) async { + final account = _accountSemaphores.putIfAbsent( + request.accountId, + () => _AsyncSemaphore(_limits.maximumConcurrentPerAccount), + ); + await account.acquire(); + _AsyncSemaphore? collection; + try { + final collectionId = request.collectionId; + if (collectionId != null) { + collection = _collectionSemaphores.putIfAbsent( + '${request.accountId}|$collectionId', + () => _AsyncSemaphore(_limits.maximumConcurrentPerCollection), + ); + await collection.acquire(); + } + return await action(); + } finally { + collection?.release(); + account.release(); + } + } + + Future _sendWithRetry( + DavRequest request, + DavBasicCredential credential, + DavCancellationToken token, + ) async { + final attempts = request.retryClass == DavRetryClass.safeRead + ? _limits.maximumReadAttempts + : 1; + Object? lastError; + for (var attempt = 1; attempt <= attempts; attempt += 1) { + token.throwIfCancelled(correlationId: request.correlationId); + try { + final response = await _sendFollowingRedirects( + request, + credential, + token, + ); + if (!_isRetryableStatus(response.statusCode) || attempt == attempts) { + return response; + } + final honorsRetryAfter = + response.statusCode == HttpStatus.tooManyRequests || + response.statusCode == HttpStatus.serviceUnavailable; + await _delay( + _retryDelay( + honorsRetryAfter ? response.headers['retry-after'] : null, + attempt, + ), + ); + } on DavException catch (error) { + lastError = error; + if (attempt == attempts || !_isRetryableException(error)) { + rethrow; + } + await _delay(_retryDelay(null, attempt)); + } on Object catch (error) { + lastError = error; + if (attempt == attempts) { + throw _networkException(error, request.correlationId); + } + await _delay(_retryDelay(null, attempt)); + } + } + throw _networkException(lastError, request.correlationId); + } + + Future _sendFollowingRedirects( + DavRequest request, + DavBasicCredential credential, + DavCancellationToken token, + ) async { + var currentUri = request.uri; + var method = request.method.toUpperCase(); + var body = request.bodyBytes; + final visited = {}; + + for ( + var redirectCount = 0; + redirectCount <= _limits.maximumRedirects; + redirectCount += 1 + ) { + token.throwIfCancelled(correlationId: request.correlationId); + if (!visited.add(currentUri.toString())) { + throw DavException( + kind: DavErrorKind.redirectLoop, + code: 'DavRedirectLoop', + safeMessage: 'The DAV server returned a redirect loop.', + correlationId: request.correlationId, + ); + } + if (!_profile.isTrustedCredentialDestination( + currentUri, + accountAuthority: _accountAuthority, + )) { + throw DavException( + kind: DavErrorKind.redirectRejected, + code: 'DavRedirectDestinationRejected', + safeMessage: 'The DAV server redirected to an untrusted destination.', + correlationId: request.correlationId, + ); + } + + final outbound = http.Request(method, currentUri) + ..followRedirects = false + ..headers.addAll(request.headers) + ..headers['authorization'] = credential.authorizationValue + ..headers['x-busymax-correlation-id'] = request.correlationId; + if (body != null) { + outbound.bodyBytes = body; + } + + final streamed = await _client + .send(outbound) + .timeout( + _limits.connectTimeout, + onTimeout: () => throw DavException( + kind: DavErrorKind.timeout, + code: 'DavConnectTimeout', + safeMessage: + 'The DAV server did not accept a connection in time.', + correlationId: request.correlationId, + ), + ); + final responseBytes = await _readBoundedBody( + streamed, + token, + request.correlationId, + ); + final headers = { + for (final entry in streamed.headers.entries) + entry.key.toLowerCase(): entry.value, + }; + + if (!_isRedirect(streamed.statusCode)) { + return DavResponse( + statusCode: streamed.statusCode, + headers: headers, + bodyBytes: responseBytes, + requestUri: currentUri, + correlationId: request.correlationId, + ); + } + final location = headers['location']; + if (location == null || location.trim().isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavRedirectMissingLocation', + safeMessage: 'The DAV server returned an invalid redirect.', + statusCode: streamed.statusCode, + correlationId: request.correlationId, + ); + } + if (redirectCount == _limits.maximumRedirects) { + throw DavException( + kind: DavErrorKind.redirectLoop, + code: 'DavRedirectLimitExceeded', + safeMessage: 'The DAV server returned too many redirects.', + correlationId: request.correlationId, + ); + } + final destination = currentUri.resolve(location.trim()); + if (streamed.statusCode == HttpStatus.seeOther && + method != 'GET' && + method != 'HEAD') { + throw DavException( + kind: DavErrorKind.redirectRejected, + code: 'DavMutationSeeOtherRejected', + safeMessage: 'The DAV server returned an unsafe mutation redirect.', + statusCode: streamed.statusCode, + correlationId: request.correlationId, + ); + } + currentUri = destination; + } + throw StateError('Unreachable redirect loop termination.'); + } + + Future _readBoundedBody( + http.StreamedResponse response, + DavCancellationToken token, + String correlationId, + ) async { + final builder = BytesBuilder(copy: false); + var length = 0; + await for (final chunk in response.stream.timeout( + _limits.responseTimeout, + )) { + token.throwIfCancelled(correlationId: correlationId); + length += chunk.length; + if (length > _limits.maximumResponseBytes) { + throw DavException( + kind: DavErrorKind.responseTooLarge, + code: 'DavResponseTooLarge', + safeMessage: 'The DAV response exceeded the configured size limit.', + statusCode: response.statusCode, + correlationId: correlationId, + ); + } + builder.add(chunk); + } + return builder.takeBytes(); + } + + bool _isRetryableStatus(int statusCode) => + statusCode == HttpStatus.tooManyRequests || + statusCode == HttpStatus.serviceUnavailable || + statusCode == HttpStatus.badGateway || + statusCode == HttpStatus.gatewayTimeout || + statusCode == HttpStatus.internalServerError; + + bool _isRetryableException(DavException error) => + error.kind == DavErrorKind.timeout || + error.kind == DavErrorKind.network || + error.kind == DavErrorKind.tls; + + Duration _retryDelay(String? retryAfter, int attempt) { + final value = retryAfter?.trim() ?? ''; + final seconds = int.tryParse(value); + if (seconds != null && seconds >= 0) { + return Duration(seconds: min(seconds, 60)); + } + if (value.isNotEmpty) { + try { + final deadline = HttpDate.parse(value).toUtc(); + final remaining = deadline.difference(_nowUtc().toUtc()); + if (remaining <= Duration.zero) return Duration.zero; + return remaining > const Duration(seconds: 60) + ? const Duration(seconds: 60) + : remaining; + } on FormatException { + // Fall through to bounded exponential backoff with jitter. + } + } + final capMilliseconds = min(8000, 250 * (1 << (attempt - 1))); + return Duration( + milliseconds: + capMilliseconds ~/ 2 + _random.nextInt(capMilliseconds ~/ 2 + 1), + ); + } + + DavException _networkException(Object? error, String correlationId) { + final tls = error is HandshakeException || error is TlsException; + return DavException( + kind: tls ? DavErrorKind.tls : DavErrorKind.network, + code: tls ? 'DavTlsFailure' : 'DavNetworkFailure', + safeMessage: tls + ? 'The DAV server TLS connection could not be verified.' + : 'The DAV server could not be reached.', + correlationId: correlationId, + ); + } +} + +bool _isRedirect(int statusCode) => + statusCode == HttpStatus.movedPermanently || + statusCode == HttpStatus.found || + statusCode == HttpStatus.seeOther || + statusCode == HttpStatus.temporaryRedirect || + statusCode == HttpStatus.permanentRedirect; + +final class _AsyncSemaphore { + _AsyncSemaphore(this.maximum) : _available = maximum { + if (maximum < 1) { + throw ArgumentError.value(maximum, 'maximum', 'Must be positive.'); + } + } + + final int maximum; + int _available; + final _waiters = >[]; + + Future acquire() { + if (_available > 0) { + _available -= 1; + return Future.value(); + } + final completer = Completer(); + _waiters.add(completer); + return completer.future; + } + + void release() { + if (_waiters.isNotEmpty) { + _waiters.removeAt(0).complete(); + return; + } + if (_available >= maximum) { + throw StateError('DAV semaphore released more often than acquired.'); + } + _available += 1; + } +} diff --git a/lib/src/dav/ical/ical_document.dart b/lib/src/dav/ical/ical_document.dart new file mode 100644 index 0000000..cf56d6a --- /dev/null +++ b/lib/src/dav/ical/ical_document.dart @@ -0,0 +1,782 @@ +import 'dart:convert'; + +import '../dav_errors.dart'; + +const icalDocumentParserVersion = 1; + +final class IcalParameter { + const IcalParameter({ + required this.name, + required this.values, + required this.wasQuoted, + }); + + final String name; + final List values; + final bool wasQuoted; + + IcalParameter copyWith({List? values, bool? wasQuoted}) => + IcalParameter( + name: name, + values: values ?? this.values, + wasQuoted: wasQuoted ?? this.wasQuoted, + ); +} + +sealed class IcalNode { + bool get isDirty; + IcalNode deepCopy(); +} + +final class IcalProperty extends IcalNode { + IcalProperty({ + required this.group, + required this.name, + required this.parameters, + required this.rawValue, + required this.originalPhysicalLines, + this.isDirty = false, + }); + + final String? group; + final String name; + final List parameters; + String rawValue; + final List originalPhysicalLines; + + @override + bool isDirty; + + String? parameterValue(String name) { + final upper = name.toUpperCase(); + for (final parameter in parameters) { + if (parameter.name == upper && parameter.values.isNotEmpty) { + return parameter.values.first; + } + } + return null; + } + + Iterable parametersNamed(String name) { + final upper = name.toUpperCase(); + return parameters.where((parameter) => parameter.name == upper); + } + + String get decodedTextValue => decodeIcalText(rawValue); + + @override + IcalProperty deepCopy() => IcalProperty( + group: group, + name: name, + parameters: [ + for (final parameter in parameters) + IcalParameter( + name: parameter.name, + values: List.of(parameter.values), + wasQuoted: parameter.wasQuoted, + ), + ], + rawValue: rawValue, + originalPhysicalLines: List.of(originalPhysicalLines), + isDirty: isDirty, + ); + + String serializeLogicalLine() { + final buffer = StringBuffer(); + if (group != null) { + buffer + ..write(group) + ..write('.'); + } + buffer.write(name); + for (final parameter in parameters) { + buffer + ..write(';') + ..write(parameter.name) + ..write('='); + for (var index = 0; index < parameter.values.length; index += 1) { + if (index > 0) buffer.write(','); + final value = parameter.values[index]; + final quote = parameter.wasQuoted || _parameterNeedsQuotes(value); + if (quote) buffer.write('"'); + buffer.write(_escapeParameterValue(value)); + if (quote) buffer.write('"'); + } + } + buffer + ..write(':') + ..write(rawValue); + return buffer.toString(); + } +} + +final class IcalComponent extends IcalNode { + IcalComponent({ + required this.name, + required this.children, + required this.originalBeginLine, + required this.originalEndLine, + this.structurallyDirty = false, + }); + + final String name; + final List children; + final String originalBeginLine; + final String originalEndLine; + bool structurallyDirty; + + @override + bool get isDirty => + structurallyDirty || children.any((child) => child.isDirty); + + Iterable get properties => children.whereType(); + Iterable get components => children.whereType(); + + Iterable propertiesNamed(String name) { + final upper = name.toUpperCase(); + return properties.where((property) => property.name == upper); + } + + IcalProperty? firstProperty(String name) => propertiesNamed(name).firstOrNull; + + Iterable componentsNamed(String name) { + final upper = name.toUpperCase(); + return components.where((component) => component.name == upper); + } + + @override + IcalComponent deepCopy() => IcalComponent( + name: name, + children: [for (final child in children) child.deepCopy()], + originalBeginLine: originalBeginLine, + originalEndLine: originalEndLine, + structurallyDirty: structurallyDirty, + ); +} + +final class IcalDocument { + IcalDocument._({required this.root, required this.originalSource}); + + factory IcalDocument.create({ + required List components, + String productIdentifier = '-//BusyMax//CalDAV Client//EN', + }) { + IcalProperty property(String name, String value) => IcalProperty( + group: null, + name: name, + parameters: const [], + rawValue: value, + originalPhysicalLines: const [], + isDirty: true, + ); + return IcalDocument._( + root: IcalComponent( + name: 'VCALENDAR', + children: [ + property('PRODID', productIdentifier), + property('VERSION', '2.0'), + property('CALSCALE', 'GREGORIAN'), + ...components, + ], + originalBeginLine: 'BEGIN:VCALENDAR', + originalEndLine: 'END:VCALENDAR', + structurallyDirty: true, + ), + originalSource: '', + ); + } + + factory IcalDocument.parse( + String source, { + int maximumUnfoldedLineBytes = 1024 * 1024, + int maximumComponents = 10000, + int maximumProperties = 200000, + }) { + if (source.isEmpty) { + throw _icalError('IcalEmptyDocument', 'The iCalendar resource is empty.'); + } + final logicalLines = _unfoldLines( + source, + maximumUnfoldedLineBytes: maximumUnfoldedLineBytes, + ); + final stack = []; + IcalComponent? root; + var components = 0; + var properties = 0; + for (final line in logicalLines) { + final parsed = _parseProperty(line); + if (parsed.name == 'BEGIN') { + final name = parsed.rawValue.trim().toUpperCase(); + if (!_validToken(name)) { + throw _icalError( + 'IcalInvalidComponentName', + 'The iCalendar resource contains an invalid component name.', + ); + } + components += 1; + if (components > maximumComponents) { + throw _icalError( + 'IcalComponentLimitExceeded', + 'The iCalendar resource contains too many components.', + ); + } + final component = IcalComponent( + name: name, + children: [], + originalBeginLine: line.logical, + originalEndLine: 'END:$name', + ); + if (stack.isEmpty) { + if (root != null) { + throw _icalError( + 'IcalMultipleRoots', + 'The iCalendar resource contains multiple root components.', + ); + } + root = component; + } else { + stack.last.children.add(component); + } + stack.add(component); + continue; + } + if (parsed.name == 'END') { + final name = parsed.rawValue.trim().toUpperCase(); + if (stack.isEmpty || stack.last.name != name) { + throw _icalError( + 'IcalMismatchedComponentEnd', + 'The iCalendar resource contains mismatched component boundaries.', + ); + } + final ended = stack.removeLast(); + ended.structurallyDirty = false; + // Preserve the original spelling/folding of the END line. + ended._setOriginalEndLineForParse(line.logical); + continue; + } + if (stack.isEmpty) { + throw _icalError( + 'IcalPropertyOutsideComponent', + 'The iCalendar resource contains data outside a component.', + ); + } + properties += 1; + if (properties > maximumProperties) { + throw _icalError( + 'IcalPropertyLimitExceeded', + 'The iCalendar resource contains too many properties.', + ); + } + stack.last.children.add(parsed); + } + if (stack.isNotEmpty || root == null) { + throw _icalError( + 'IcalUnclosedComponent', + 'The iCalendar resource contains an unclosed component.', + ); + } + if (root.name != 'VCALENDAR') { + throw _icalError( + 'IcalExpectedVcalendar', + 'The calendar object resource is not a VCALENDAR.', + ); + } + return IcalDocument._(root: root, originalSource: source); + } + + final IcalComponent root; + final String originalSource; + + bool get isDirty => root.isDirty; + + Iterable get calendarComponents => root.components; + + IcalDocument deepCopy() => + IcalDocument._(root: root.deepCopy(), originalSource: originalSource); + + String serialize({bool canonicalizeUntouched = false}) { + if (!isDirty && !canonicalizeUntouched) { + return originalSource; + } + final output = StringBuffer(); + void writeComponent(IcalComponent component) { + output + ..write(_foldContentLine('BEGIN:${component.name}')) + ..write('\r\n'); + for (final child in component.children) { + switch (child) { + case final IcalProperty property: + if (!property.isDirty && !canonicalizeUntouched) { + final logical = property.originalPhysicalLines.isEmpty + ? property.serializeLogicalLine() + : _unfoldPhysicalLines(property.originalPhysicalLines); + output + ..write(_foldContentLine(logical)) + ..write('\r\n'); + } else { + output + ..write(_foldContentLine(property.serializeLogicalLine())) + ..write('\r\n'); + } + case final IcalComponent nested: + writeComponent(nested); + } + } + output + ..write(_foldContentLine('END:${component.name}')) + ..write('\r\n'); + } + + writeComponent(root); + return output.toString(); + } +} + +String _unfoldPhysicalLines(List physicalLines) { + final output = StringBuffer(physicalLines.first); + for (final line in physicalLines.skip(1)) { + output.write( + line.startsWith(' ') || line.startsWith('\t') ? line.substring(1) : line, + ); + } + return output.toString(); +} + +extension on IcalComponent { + void _setOriginalEndLineForParse(String value) { + // `originalEndLine` is intentionally informational. Components serialize + // canonical BEGIN/END markers after mutation, while an untouched document + // returns the byte-for-byte original source. No state update is required. + if (value.isEmpty) { + throw StateError('An END content line cannot be empty.'); + } + } +} + +final class IcalComponentKey { + const IcalComponentKey({ + required this.componentType, + required this.uid, + this.recurrenceIdKey, + }); + + final String componentType; + final String uid; + final String? recurrenceIdKey; +} + +final class IcalDocumentPatcher { + IcalDocumentPatcher(this.document); + + final IcalDocument document; + + IcalComponent requireComponent(IcalComponentKey key) { + final matches = document.calendarComponents + .where((component) { + if (component.name != key.componentType.toUpperCase()) return false; + if (component.firstProperty('UID')?.rawValue != key.uid) return false; + return icalRecurrenceIdKey( + component.firstProperty('RECURRENCE-ID'), + ) == + key.recurrenceIdKey; + }) + .toList(growable: false); + if (matches.length != 1) { + throw _icalError( + 'IcalTargetComponentAmbiguous', + 'The requested iCalendar component could not be identified uniquely.', + ); + } + return matches.single; + } + + void replaceSingletonRaw( + IcalComponentKey key, + String propertyName, + String? rawValue, { + List parameters = const [], + }) { + final component = requireComponent(key); + _replaceSingleton( + component, + propertyName, + rawValue, + parameters: parameters, + ); + } + + void replaceSingletonText( + IcalComponentKey key, + String propertyName, + String? value, + ) { + replaceSingletonRaw( + key, + propertyName, + value == null ? null : encodeIcalText(value), + ); + } + + void replaceRepeatedRaw( + IcalComponentKey key, + String propertyName, + List<({String value, List parameters})> values, + ) { + final component = requireComponent(key); + final upper = _validatePropertyName(propertyName); + final indexes = []; + for (var index = 0; index < component.children.length; index += 1) { + final node = component.children[index]; + if (node is IcalProperty && node.name == upper) indexes.add(index); + } + final insertionIndex = indexes.firstOrNull ?? component.children.length; + component.children.removeWhere( + (node) => node is IcalProperty && node.name == upper, + ); + component.children.insertAll( + insertionIndex, + values.map( + (entry) => IcalProperty( + group: null, + name: upper, + parameters: List.of(entry.parameters), + rawValue: entry.value, + originalPhysicalLines: const [], + isDirty: true, + ), + ), + ); + component.structurallyDirty = true; + } + + void addComponent(IcalComponent component) { + document.root.children.add(component); + document.root.structurallyDirty = true; + } + + void removeComponent(IcalComponentKey key) { + final component = requireComponent(key); + document.root.children.remove(component); + document.root.structurallyDirty = true; + } + + void _replaceSingleton( + IcalComponent component, + String propertyName, + String? rawValue, { + required List parameters, + }) { + final upper = _validatePropertyName(propertyName); + final indexes = []; + for (var index = 0; index < component.children.length; index += 1) { + final child = component.children[index]; + if (child is IcalProperty && child.name == upper) indexes.add(index); + } + if (rawValue == null) { + if (indexes.isNotEmpty) { + component.children.removeWhere( + (node) => node is IcalProperty && node.name == upper, + ); + component.structurallyDirty = true; + } + return; + } + final replacement = IcalProperty( + group: null, + name: upper, + parameters: List.of(parameters), + rawValue: rawValue, + originalPhysicalLines: const [], + isDirty: true, + ); + if (indexes.isEmpty) { + final endOfIdentity = component.children.lastIndexWhere( + (node) => + node is IcalProperty && + const { + 'UID', + 'RECURRENCE-ID', + 'DTSTAMP', + 'SEQUENCE', + }.contains(node.name), + ); + component.children.insert(endOfIdentity + 1, replacement); + } else { + component.children[indexes.first] = replacement; + for (final index in indexes.skip(1).toList().reversed) { + component.children.removeAt(index); + } + } + component.structurallyDirty = true; + } +} + +String? icalRecurrenceIdKey(IcalProperty? property) { + if (property == null) return null; + final valueKind = property.parameterValue('VALUE')?.toUpperCase(); + final tzid = property.parameterValue('TZID'); + final range = property.parameterValue('RANGE')?.toUpperCase(); + return [ + if (valueKind != null) 'VALUE=$valueKind', + if (tzid != null) 'TZID=$tzid', + if (range != null) 'RANGE=$range', + property.rawValue, + ].join(':'); +} + +String decodeIcalText(String source) { + final output = StringBuffer(); + for (var index = 0; index < source.length; index += 1) { + final character = source[index]; + if (character != r'\' || index + 1 >= source.length) { + output.write(character); + continue; + } + final escaped = source[index += 1]; + output.write(switch (escaped) { + 'n' || 'N' => '\n', + ',' => ',', + ';' => ';', + r'\' => r'\', + _ => escaped, + }); + } + return output.toString(); +} + +String encodeIcalText(String source) => source + .replaceAll(r'\', r'\\') + .replaceAll('\r\n', r'\n') + .replaceAll('\r', r'\n') + .replaceAll('\n', r'\n') + .replaceAll(';', r'\;') + .replaceAll(',', r'\,'); + +final class _UnfoldedLine { + const _UnfoldedLine({required this.logical, required this.physical}); + + final String logical; + final List physical; +} + +List<_UnfoldedLine> _unfoldLines( + String source, { + required int maximumUnfoldedLineBytes, +}) { + final physicalLines = source.split(RegExp(r'\r\n|\n|\r')); + if (physicalLines.isNotEmpty && physicalLines.last.isEmpty) { + physicalLines.removeLast(); + } + final result = <_UnfoldedLine>[]; + for (final physical in physicalLines) { + if ((physical.startsWith(' ') || physical.startsWith('\t')) && + result.isNotEmpty) { + final previous = result.removeLast(); + final logical = '${previous.logical}${physical.substring(1)}'; + _checkLineLength(logical, maximumUnfoldedLineBytes); + result.add( + _UnfoldedLine( + logical: logical, + physical: [...previous.physical, physical], + ), + ); + } else { + _checkLineLength(physical, maximumUnfoldedLineBytes); + result.add(_UnfoldedLine(logical: physical, physical: [physical])); + } + } + return result; +} + +void _checkLineLength(String value, int maximumBytes) { + if (utf8.encode(value).length > maximumBytes) { + throw _icalError( + 'IcalContentLineLimitExceeded', + 'An iCalendar content line exceeded the configured size limit.', + ); + } +} + +IcalProperty _parseProperty(_UnfoldedLine source) { + final colon = _findUnquoted(source.logical, ':'); + if (colon <= 0) { + throw _icalError( + 'IcalMalformedContentLine', + 'The iCalendar resource contains a malformed content line.', + ); + } + final prefix = source.logical.substring(0, colon); + final rawValue = source.logical.substring(colon + 1); + final segments = _splitUnquoted(prefix, ';'); + final namePart = segments.removeAt(0); + final dot = namePart.indexOf('.'); + final group = dot < 0 ? null : namePart.substring(0, dot); + final name = (dot < 0 ? namePart : namePart.substring(dot + 1)).toUpperCase(); + if (!_validToken(name) || (group != null && !_validToken(group))) { + throw _icalError( + 'IcalInvalidPropertyName', + 'The iCalendar resource contains an invalid property name.', + ); + } + final parameters = []; + for (final segment in segments) { + final equals = _findUnquoted(segment, '='); + if (equals <= 0) { + throw _icalError( + 'IcalMalformedParameter', + 'The iCalendar resource contains a malformed property parameter.', + ); + } + final parameterName = segment.substring(0, equals).toUpperCase(); + if (!_validToken(parameterName)) { + throw _icalError( + 'IcalInvalidParameterName', + 'The iCalendar resource contains an invalid parameter name.', + ); + } + final rawParameterValue = segment.substring(equals + 1); + final quoted = + rawParameterValue.length >= 2 && + rawParameterValue.startsWith('"') && + rawParameterValue.endsWith('"'); + final unquoted = quoted + ? rawParameterValue.substring(1, rawParameterValue.length - 1) + : rawParameterValue; + parameters.add( + IcalParameter( + name: parameterName, + values: quoted + ? [_unescapeParameterValue(unquoted)] + : _splitUnquoted( + unquoted, + ',', + ).map(_unescapeParameterValue).toList(growable: false), + wasQuoted: quoted, + ), + ); + } + return IcalProperty( + group: group, + name: name, + parameters: parameters, + rawValue: rawValue, + originalPhysicalLines: source.physical, + ); +} + +int _findUnquoted(String source, String character) { + var quoted = false; + var escaped = false; + for (var index = 0; index < source.length; index += 1) { + final value = source[index]; + if (escaped) { + escaped = false; + continue; + } + if (value == r'\') { + escaped = true; + continue; + } + if (value == '"') { + quoted = !quoted; + continue; + } + if (!quoted && value == character) return index; + } + return -1; +} + +List _splitUnquoted(String source, String separator) { + final result = []; + var start = 0; + var quoted = false; + var escaped = false; + for (var index = 0; index < source.length; index += 1) { + final value = source[index]; + if (escaped) { + escaped = false; + continue; + } + if (value == r'\') { + escaped = true; + continue; + } + if (value == '"') { + quoted = !quoted; + } else if (!quoted && value == separator) { + result.add(source.substring(start, index)); + start = index + 1; + } + } + if (quoted) { + throw _icalError( + 'IcalUnclosedParameterQuote', + 'The iCalendar resource contains an unclosed parameter quote.', + ); + } + result.add(source.substring(start)); + return result; +} + +String _foldContentLine(String logical) { + final runes = logical.runes.toList(growable: false); + final lines = []; + var current = StringBuffer(); + var currentBytes = 0; + var limit = 75; + for (final rune in runes) { + final value = String.fromCharCode(rune); + final bytes = utf8.encode(value).length; + if (currentBytes + bytes > limit && currentBytes > 0) { + lines.add(current.toString()); + current = StringBuffer(); + currentBytes = 0; + limit = 74; + } + current.write(value); + currentBytes += bytes; + } + lines.add(current.toString()); + return lines.join('\r\n '); +} + +String _escapeParameterValue(String value) => + value.replaceAll('^', '^^').replaceAll('\n', '^n').replaceAll('"', "^'"); + +String _unescapeParameterValue(String value) { + final output = StringBuffer(); + for (var index = 0; index < value.length; index += 1) { + if (value[index] != '^' || index + 1 >= value.length) { + output.write(value[index]); + continue; + } + final escaped = value[index += 1]; + output.write(switch (escaped) { + '^' => '^', + 'n' || 'N' => '\n', + "'" => '"', + _ => '^$escaped', + }); + } + return output.toString(); +} + +bool _parameterNeedsQuotes(String value) => + value.isEmpty || value.contains(RegExp(r'[:;,]')) || value.trim() != value; + +String _validatePropertyName(String value) { + final upper = value.toUpperCase(); + if (!_validToken(upper)) { + throw ArgumentError.value(value, 'propertyName', 'Invalid iCalendar name.'); + } + return upper; +} + +bool _validToken(String value) => + value.isNotEmpty && RegExp(r'^[A-Za-z0-9-]+$').hasMatch(value); + +DavException _icalError(String code, String safeMessage) => DavException( + kind: DavErrorKind.invalidCalendarData, + code: code, + safeMessage: safeMessage, +); diff --git a/lib/src/dav/ical/ical_recurrence.dart b/lib/src/dav/ical/ical_recurrence.dart new file mode 100644 index 0000000..720d034 --- /dev/null +++ b/lib/src/dav/ical/ical_recurrence.dart @@ -0,0 +1,979 @@ +import 'dart:math' as math; + +import '../dav_errors.dart'; +import 'ical_document.dart'; +import 'ical_semantics.dart'; +import 'ical_timezone.dart'; + +const icalOccurrenceProjectionVersion = 1; + +final class IcalRecurrenceLimits { + const IcalRecurrenceLimits({ + this.maximumOccurrences = 10000, + this.maximumPeriods = 1000000, + this.maximumRuleValues = 1024, + this.maximumProjectionRange = const Duration(days: 366 * 20), + }); + + final int maximumOccurrences; + final int maximumPeriods; + final int maximumRuleValues; + final Duration maximumProjectionRange; +} + +/// One projected occurrence. [recurrenceId] is the original scheduled start; +/// [start] may differ when an exception moves the occurrence. +final class IcalOccurrence { + const IcalOccurrence({ + required this.master, + required this.override, + required this.recurrenceId, + required this.start, + required this.end, + required this.occurrenceKey, + }); + + final IcalSemanticComponent master; + final IcalSemanticComponent? override; + final IcalTemporalValue recurrenceId; + final IcalTemporalValue start; + final IcalTemporalValue? end; + final String occurrenceKey; + + IcalSemanticComponent get effectiveComponent => override ?? master; + bool get isException => override != null; + bool get isCancelled => override?.isCancelled ?? master.isCancelled; + + String? get summary => override?.summary ?? master.summary; + String? get description => override?.description ?? master.description; + String? get location => override?.location ?? master.location; +} + +/// Expands an RFC 5545 recurrence set from its authoritative resource. +/// +/// The expansion is deliberately range-bound and count-bound. Native DATE, +/// floating, UTC, and TZID wall-clock forms remain intact in the returned +/// values; TZID comparisons use the bundled IANA database when possible. +final class IcalRecurrenceExpander { + IcalRecurrenceExpander({this.limits = const IcalRecurrenceLimits()}); + + final IcalRecurrenceLimits limits; + + /// Returns the next RRULE occurrence after [current]. + /// + /// Nextcloud Tasks advances a recurring VTODO from its current DTSTART or + /// DUE and its first RRULE. This helper follows that same rule-local + /// iteration model without imposing a calendar projection window. + IcalTemporalValue? nextTaskOccurrence( + IcalSemanticDocument document, { + required IcalTemporalValue current, + required String recurrenceRule, + }) { + final resolver = IcalTimeZoneResolver.fromDocument(document); + final rule = _RecurrenceRule.parse( + recurrenceRule, + anchor: current, + maximumValues: limits.maximumRuleValues, + ); + var generatedForCount = 0; + for (var period = 0; period < limits.maximumPeriods; period += 1) { + final candidates = rule.candidatesForPeriod(current.localValue, period); + for (final wallValue in candidates) { + if (wallValue.isBefore(current.localValue)) continue; + final value = _withWallValue(current, wallValue); + if (rule.until != null && _afterUntil(value, rule.until!, resolver)) { + return null; + } + generatedForCount += 1; + if (rule.count != null && generatedForCount > rule.count!) return null; + if (wallValue.isAfter(current.localValue)) return value; + } + } + throw _limitError( + 'IcalRecurrenceIterationLimitExceeded', + 'A recurrence rule exceeded the safe expansion limit.', + ); + } + + List expand( + IcalSemanticDocument document, { + required DateTime rangeStartUtc, + required DateTime rangeEndUtc, + }) { + final rangeStart = rangeStartUtc.toUtc(); + final rangeEnd = rangeEndUtc.toUtc(); + if (!rangeEnd.isAfter(rangeStart)) { + throw ArgumentError('The recurrence projection range must be positive.'); + } + if (rangeEnd.difference(rangeStart) > limits.maximumProjectionRange) { + throw _limitError( + 'IcalProjectionRangeLimitExceeded', + 'The requested recurrence projection range is too large.', + ); + } + final timeZoneResolver = IcalTimeZoneResolver.fromDocument(document); + + final masters = document.components + .where((component) => component.recurrenceId == null) + .toList(growable: false); + if (masters.length != 1) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalRecurrenceMasterInvariantFailed', + safeMessage: 'A recurrence set must contain exactly one master.', + ); + } + final master = masters.single; + final anchor = master.start ?? master.due; + if (anchor == null) return const []; + + final starts = { + _temporalIdentity(anchor): anchor, + }; + for (final ruleText in master.recurrenceRules) { + final rule = _RecurrenceRule.parse( + ruleText, + anchor: anchor, + maximumValues: limits.maximumRuleValues, + ); + _addRuleStarts(starts, rule, anchor, rangeEnd, timeZoneResolver); + } + for (final property in master.documentComponent.propertiesNamed('RDATE')) { + for (final value in _parseDateList(property, anchor)) { + starts[_temporalIdentity(value)] = value; + } + } + for (final property in master.documentComponent.propertiesNamed('EXDATE')) { + for (final value in _parseDateList(property, anchor)) { + starts.remove(_temporalIdentity(value)); + } + } + + final overrides = {}; + for (final component in document.components) { + final recurrenceId = component.recurrenceId; + if (recurrenceId == null) continue; + final key = _temporalIdentity(recurrenceId); + if (overrides.containsKey(key)) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalDuplicateRecurrenceId', + safeMessage: + 'A recurrence set contains duplicate recurrence exceptions.', + ); + } + overrides[key] = component; + // Retain detached exceptions even when a malformed or changed server + // rule no longer generates their original RECURRENCE-ID. + starts.putIfAbsent(key, () => recurrenceId); + } + + if (starts.length > limits.maximumOccurrences) { + throw _occurrenceLimitError(); + } + final masterDuration = _componentDuration(master, timeZoneResolver); + final result = []; + for (final entry in starts.entries) { + final originalStart = entry.value; + final exception = overrides[entry.key]; + final effectiveStart = + exception?.start ?? exception?.due ?? originalStart; + final effectiveEnd = _occurrenceEnd( + exception, + effectiveStart, + masterDuration, + ); + if (!_overlaps( + effectiveStart, + effectiveEnd, + rangeStart, + rangeEnd, + timeZoneResolver, + )) { + continue; + } + result.add( + IcalOccurrence( + master: master, + override: exception, + recurrenceId: originalStart, + start: effectiveStart, + end: effectiveEnd, + occurrenceKey: entry.key, + ), + ); + if (result.length > limits.maximumOccurrences) { + throw _occurrenceLimitError(); + } + } + result.sort( + (left, right) => icalTemporalToUtc( + left.start, + resolver: timeZoneResolver, + ).compareTo(icalTemporalToUtc(right.start, resolver: timeZoneResolver)), + ); + return List.unmodifiable(result); + } + + void _addRuleStarts( + Map starts, + _RecurrenceRule rule, + IcalTemporalValue anchor, + DateTime rangeEndUtc, + IcalTimeZoneResolver timeZoneResolver, + ) { + var generatedForCount = 0; + var reachedEnd = false; + for (var period = 0; period < limits.maximumPeriods; period += 1) { + final candidates = rule.candidatesForPeriod(anchor.localValue, period); + if (candidates.isEmpty && + rule.periodStartsAfter( + period, + rangeEndUtc, + anchor, + timeZoneResolver, + )) { + reachedEnd = true; + break; + } + for (final wallValue in candidates) { + if (wallValue.isBefore(anchor.localValue)) continue; + final value = _withWallValue(anchor, wallValue); + if (rule.until != null && + _afterUntil(value, rule.until!, timeZoneResolver)) { + reachedEnd = true; + break; + } + generatedForCount += 1; + if (rule.count != null && generatedForCount > rule.count!) { + reachedEnd = true; + break; + } + starts[_temporalIdentity(value)] = value; + if (starts.length > limits.maximumOccurrences) { + throw _occurrenceLimitError(); + } + } + if (reachedEnd) break; + if (rule.periodStartsAfter( + period + 1, + rangeEndUtc, + anchor, + timeZoneResolver, + )) { + reachedEnd = true; + break; + } + } + if (!reachedEnd) { + throw _limitError( + 'IcalRecurrenceIterationLimitExceeded', + 'A recurrence rule exceeded the safe expansion limit.', + ); + } + } +} + +enum _Frequency { secondly, minutely, hourly, daily, weekly, monthly, yearly } + +final class _ByDay { + const _ByDay(this.ordinal, this.weekday); + + final int? ordinal; + final int weekday; +} + +final class _RecurrenceRule { + _RecurrenceRule({ + required this.frequency, + required this.interval, + required this.count, + required this.until, + required this.weekStart, + required this.bySecond, + required this.byMinute, + required this.byHour, + required this.byDay, + required this.byMonthDay, + required this.byYearDay, + required this.byWeekNumber, + required this.byMonth, + required this.bySetPosition, + }); + + factory _RecurrenceRule.parse( + String source, { + required IcalTemporalValue anchor, + required int maximumValues, + }) { + final values = {}; + for (final segment in source.split(';')) { + final separator = segment.indexOf('='); + if (separator <= 0 || separator == segment.length - 1) { + throw _invalidRule(); + } + final key = segment.substring(0, separator).toUpperCase(); + if (values.containsKey(key)) throw _invalidRule(); + values[key] = segment.substring(separator + 1).toUpperCase(); + } + const supported = { + 'FREQ', + 'UNTIL', + 'COUNT', + 'INTERVAL', + 'BYSECOND', + 'BYMINUTE', + 'BYHOUR', + 'BYDAY', + 'BYMONTHDAY', + 'BYYEARDAY', + 'BYWEEKNO', + 'BYMONTH', + 'BYSETPOS', + 'WKST', + }; + if (values.keys.any((key) => !supported.contains(key))) { + throw const DavException( + kind: DavErrorKind.unsupportedComponent, + code: 'IcalUnsupportedRecurrencePart', + safeMessage: 'The recurrence rule uses an unsupported rule part.', + ); + } + final frequency = switch (values['FREQ']) { + 'SECONDLY' => _Frequency.secondly, + 'MINUTELY' => _Frequency.minutely, + 'HOURLY' => _Frequency.hourly, + 'DAILY' => _Frequency.daily, + 'WEEKLY' => _Frequency.weekly, + 'MONTHLY' => _Frequency.monthly, + 'YEARLY' => _Frequency.yearly, + _ => throw _invalidRule(), + }; + final count = _positiveInteger(values['COUNT']); + if (count != null && values.containsKey('UNTIL')) throw _invalidRule(); + final interval = _positiveInteger(values['INTERVAL']) ?? 1; + final byDay = _parseByDay(values['BYDAY'], maximumValues); + if (frequency == _Frequency.weekly && + byDay.any((day) => day.ordinal != null)) { + throw _invalidRule(); + } + return _RecurrenceRule( + frequency: frequency, + interval: interval, + count: count, + until: values['UNTIL'] == null + ? null + : _parseTemporalToken(values['UNTIL']!, anchor), + weekStart: _weekday(values['WKST'] ?? 'MO'), + bySecond: _integerList( + values['BYSECOND'], + minimum: 0, + maximum: 59, + maximumValues: maximumValues, + ), + byMinute: _integerList( + values['BYMINUTE'], + minimum: 0, + maximum: 59, + maximumValues: maximumValues, + ), + byHour: _integerList( + values['BYHOUR'], + minimum: 0, + maximum: 23, + maximumValues: maximumValues, + ), + byDay: byDay, + byMonthDay: _integerList( + values['BYMONTHDAY'], + minimum: -31, + maximum: 31, + disallowZero: true, + maximumValues: maximumValues, + ), + byYearDay: _integerList( + values['BYYEARDAY'], + minimum: -366, + maximum: 366, + disallowZero: true, + maximumValues: maximumValues, + ), + byWeekNumber: _integerList( + values['BYWEEKNO'], + minimum: -53, + maximum: 53, + disallowZero: true, + maximumValues: maximumValues, + ), + byMonth: _integerList( + values['BYMONTH'], + minimum: 1, + maximum: 12, + maximumValues: maximumValues, + ), + bySetPosition: _integerList( + values['BYSETPOS'], + minimum: -366, + maximum: 366, + disallowZero: true, + maximumValues: maximumValues, + ), + ); + } + + final _Frequency frequency; + final int interval; + final int? count; + final IcalTemporalValue? until; + final int weekStart; + final List bySecond; + final List byMinute; + final List byHour; + final List<_ByDay> byDay; + final List byMonthDay; + final List byYearDay; + final List byWeekNumber; + final List byMonth; + final List bySetPosition; + + List candidatesForPeriod(DateTime start, int period) { + final candidates = switch (frequency) { + _Frequency.secondly => _secondCandidates(start, period), + _Frequency.minutely => _minuteCandidates(start, period), + _Frequency.hourly => _hourCandidates(start, period), + _Frequency.daily => _dailyCandidates(start, period), + _Frequency.weekly => _weeklyCandidates(start, period), + _Frequency.monthly => _monthlyCandidates(start, period), + _Frequency.yearly => _yearlyCandidates(start, period), + }; + final filtered = candidates.where(_matchesAllFilters).toSet().toList() + ..sort(); + if (bySetPosition.isEmpty) return filtered; + final selected = {}; + for (final position in bySetPosition) { + final index = position > 0 ? position - 1 : filtered.length + position; + if (index >= 0 && index < filtered.length) selected.add(filtered[index]); + } + return selected.toList()..sort(); + } + + bool periodStartsAfter( + int period, + DateTime rangeEndUtc, + IcalTemporalValue anchor, + IcalTimeZoneResolver timeZoneResolver, + ) { + final wall = _periodAnchor(anchor.localValue, period); + return icalTemporalToUtc( + _withWallValue(anchor, wall), + resolver: timeZoneResolver, + ).isAfter(rangeEndUtc); + } + + DateTime _periodAnchor(DateTime start, int period) => switch (frequency) { + _Frequency.secondly => start.add(Duration(seconds: period * interval)), + _Frequency.minutely => start.add(Duration(minutes: period * interval)), + _Frequency.hourly => start.add(Duration(hours: period * interval)), + _Frequency.daily => _addDays(start, period * interval), + _Frequency.weekly => _addDays(start, period * interval * 7), + _Frequency.monthly => _addMonths(start, period * interval), + _Frequency.yearly => _addYears(start, period * interval), + }; + + List _secondCandidates(DateTime start, int period) => [ + _periodAnchor(start, period), + ]; + + List _minuteCandidates(DateTime start, int period) { + final anchor = _periodAnchor(start, period); + return [ + for (final second in bySecond.isEmpty ? [start.second] : bySecond) + _wall( + anchor.year, + anchor.month, + anchor.day, + anchor.hour, + anchor.minute, + second, + ), + ]; + } + + List _hourCandidates(DateTime start, int period) { + final anchor = _periodAnchor(start, period); + return [ + for (final minute in byMinute.isEmpty ? [start.minute] : byMinute) + for (final second in bySecond.isEmpty ? [start.second] : bySecond) + _wall( + anchor.year, + anchor.month, + anchor.day, + anchor.hour, + minute, + second, + ), + ]; + } + + List _dailyCandidates(DateTime start, int period) { + final date = _periodAnchor(start, period); + return _timesForDate(date, start); + } + + List _weeklyCandidates(DateTime start, int period) { + final anchor = _periodAnchor(start, period); + final week = _startOfWeek(anchor, weekStart); + final weekdays = byDay.isEmpty + ? [start.weekday] + : byDay.map((value) => value.weekday).toSet().toList(); + return [ + for (final weekday in weekdays) + ..._timesForDate(_addDays(week, (weekday - weekStart) % 7), start), + ]; + } + + List _monthlyCandidates(DateTime start, int period) { + final anchor = _periodAnchor(start, period); + if (byMonth.isNotEmpty && !byMonth.contains(anchor.month)) return const []; + return [ + for (final day in _monthDays(anchor.year, anchor.month, start.day)) + ..._timesForDate(_wall(anchor.year, anchor.month, day), start), + ]; + } + + List _yearlyCandidates(DateTime start, int period) { + final anchor = _periodAnchor(start, period); + final year = anchor.year; + final dates = []; + if (byYearDay.isNotEmpty) { + final days = _daysInYear(year); + for (final value in byYearDay) { + final day = value > 0 ? value : days + value + 1; + if (day >= 1 && day <= days) { + dates.add(_wall(year).add(Duration(days: day - 1))); + } + } + } else if (byWeekNumber.isNotEmpty) { + for (var day = 1; day <= _daysInYear(year); day += 1) { + final date = _wall(year).add(Duration(days: day - 1)); + if (_matchesWeekNumber(date)) dates.add(date); + } + } else { + final months = byMonth.isEmpty ? [start.month] : byMonth; + for (final month in months) { + for (final day in _monthDays(year, month, start.day)) { + dates.add(_wall(year, month, day)); + } + } + if (byDay.isNotEmpty && byMonth.isEmpty) { + dates + ..clear() + ..addAll([ + for (var day = 1; day <= _daysInYear(year); day += 1) + if (_matchesByDayInYear(_wall(year).add(Duration(days: day - 1)))) + _wall(year).add(Duration(days: day - 1)), + ]); + } + } + return [for (final date in dates) ..._timesForDate(date, start)]; + } + + List _monthDays(int year, int month, int defaultDay) { + final days = _daysInMonth(year, month); + final hasDaySelector = byMonthDay.isNotEmpty || byDay.isNotEmpty; + if (!hasDaySelector) return defaultDay <= days ? [defaultDay] : const []; + return [ + for (var day = 1; day <= days; day += 1) + if (_matchesMonthDay(day, days) && + _matchesByDayInMonth(_wall(year, month, day))) + day, + ]; + } + + List _timesForDate(DateTime date, DateTime start) => [ + for (final hour in byHour.isEmpty ? [start.hour] : byHour) + for (final minute in byMinute.isEmpty ? [start.minute] : byMinute) + for (final second in bySecond.isEmpty ? [start.second] : bySecond) + _wall(date.year, date.month, date.day, hour, minute, second), + ]; + + bool _matchesAllFilters(DateTime date) { + if (byMonth.isNotEmpty && !byMonth.contains(date.month)) return false; + if (!_matchesMonthDay(date.day, _daysInMonth(date.year, date.month))) { + return false; + } + if (!_matchesYearDay(date)) return false; + if (!_matchesWeekNumber(date)) return false; + if (byHour.isNotEmpty && !byHour.contains(date.hour)) return false; + if (byMinute.isNotEmpty && !byMinute.contains(date.minute)) return false; + if (bySecond.isNotEmpty && !bySecond.contains(date.second)) return false; + if (byDay.isEmpty) return true; + return switch (frequency) { + _Frequency.yearly when byMonth.isEmpty => _matchesByDayInYear(date), + _Frequency.monthly || _Frequency.yearly => _matchesByDayInMonth(date), + _ => byDay.any((value) => value.weekday == date.weekday), + }; + } + + bool _matchesMonthDay(int day, int daysInMonth) { + if (byMonthDay.isEmpty) return true; + return byMonthDay.any( + (value) => (value > 0 ? value : daysInMonth + value + 1) == day, + ); + } + + bool _matchesYearDay(DateTime date) { + if (byYearDay.isEmpty) return true; + final ordinal = date.difference(_wall(date.year)).inDays + 1; + final days = _daysInYear(date.year); + return byYearDay.any( + (value) => (value > 0 ? value : days + value + 1) == ordinal, + ); + } + + bool _matchesWeekNumber(DateTime date) { + if (byWeekNumber.isEmpty) return true; + final week = _weekOfYear(date, weekStart); + if (week.year != date.year) return false; + final total = _weeksInYear(date.year, weekStart); + return byWeekNumber.any( + (value) => (value > 0 ? value : total + value + 1) == week.week, + ); + } + + bool _matchesByDayInMonth(DateTime date) { + if (byDay.isEmpty) return true; + return byDay.any((selector) { + if (selector.weekday != date.weekday) return false; + final ordinal = selector.ordinal; + if (ordinal == null) return true; + if (ordinal > 0) return ((date.day - 1) ~/ 7) + 1 == ordinal; + final days = _daysInMonth(date.year, date.month); + return -(((days - date.day) ~/ 7) + 1) == ordinal; + }); + } + + bool _matchesByDayInYear(DateTime date) { + if (byDay.isEmpty) return true; + return byDay.any((selector) { + if (selector.weekday != date.weekday) return false; + final ordinal = selector.ordinal; + if (ordinal == null) return true; + final day = date.difference(_wall(date.year)).inDays + 1; + if (ordinal > 0) return ((day - 1) ~/ 7) + 1 == ordinal; + final days = _daysInYear(date.year); + return -(((days - day) ~/ 7) + 1) == ordinal; + }); + } +} + +List _parseDateList( + IcalProperty property, + IcalTemporalValue anchor, +) { + final result = []; + for (final rawEntry in property.rawValue.split(',')) { + final rawStart = rawEntry.split('/').first.trim(); + if (rawStart.isEmpty) throw _invalidRule(); + final parameters = property.parameters.isEmpty + ? _parametersForPrototype(anchor) + : property.parameters; + result.add( + parseIcalTemporal( + IcalProperty( + group: null, + name: property.name, + parameters: parameters, + rawValue: rawStart, + originalPhysicalLines: const [], + ), + )!, + ); + } + return result; +} + +List _parametersForPrototype(IcalTemporalValue prototype) => + switch (prototype.kind) { + IcalTemporalKind.date => const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ], + IcalTemporalKind.tzidDateTime => [ + IcalParameter( + name: 'TZID', + values: [prototype.timeZoneId!], + wasQuoted: false, + ), + ], + IcalTemporalKind.utcDateTime || + IcalTemporalKind.floatingDateTime => const [], + }; + +IcalTemporalValue _parseTemporalToken( + String source, + IcalTemporalValue prototype, +) { + final parameters = source.endsWith('Z') + ? const [] + : _parametersForPrototype(prototype); + return parseIcalTemporal( + IcalProperty( + group: null, + name: 'UNTIL', + parameters: parameters, + rawValue: source, + originalPhysicalLines: const [], + ), + )!; +} + +IcalTemporalValue _withWallValue(IcalTemporalValue prototype, DateTime wall) => + IcalTemporalValue( + rawValue: _formatWallValue(wall, prototype.kind), + kind: prototype.kind, + localValue: wall, + timeZoneId: prototype.timeZoneId, + ); + +String _formatWallValue(DateTime value, IcalTemporalKind kind) { + String two(int number) => number.toString().padLeft(2, '0'); + final date = + '${value.year.toString().padLeft(4, '0')}' + '${two(value.month)}${two(value.day)}'; + if (kind == IcalTemporalKind.date) return date; + final dateTime = + '${date}T${two(value.hour)}${two(value.minute)}' + '${two(value.second)}'; + return kind == IcalTemporalKind.utcDateTime ? '${dateTime}Z' : dateTime; +} + +String _temporalIdentity(IcalTemporalValue value) => switch (value.kind) { + IcalTemporalKind.date => 'DATE:${value.rawValue}', + IcalTemporalKind.floatingDateTime => 'FLOATING:${value.rawValue}', + IcalTemporalKind.utcDateTime => + 'UTC:${icalTemporalToUtc(value).toIso8601String()}', + IcalTemporalKind.tzidDateTime => 'TZID=${value.timeZoneId}:${value.rawValue}', +}; + +Duration _componentDuration( + IcalSemanticComponent component, + IcalTimeZoneResolver timeZoneResolver, +) { + if (component.duration != null) return component.duration!.duration; + final start = component.start ?? component.due; + final end = component.end; + if (start == null || end == null) return Duration.zero; + if (start.kind == IcalTemporalKind.tzidDateTime && + end.kind == IcalTemporalKind.tzidDateTime && + start.timeZoneId == end.timeZoneId) { + return end.localValue.difference(start.localValue); + } + return icalTemporalToUtc( + end, + resolver: timeZoneResolver, + ).difference(icalTemporalToUtc(start, resolver: timeZoneResolver)); +} + +IcalTemporalValue? _occurrenceEnd( + IcalSemanticComponent? exception, + IcalTemporalValue start, + Duration masterDuration, +) { + final explicitEnd = exception?.end; + if (explicitEnd != null) return explicitEnd; + final duration = exception?.duration?.duration ?? masterDuration; + if (duration == Duration.zero) return null; + return _withWallValue(start, start.localValue.add(duration)); +} + +bool _overlaps( + IcalTemporalValue start, + IcalTemporalValue? end, + DateTime rangeStartUtc, + DateTime rangeEndUtc, + IcalTimeZoneResolver timeZoneResolver, +) { + final startInstant = icalTemporalToUtc(start, resolver: timeZoneResolver); + final endInstant = end == null + ? startInstant + : icalTemporalToUtc(end, resolver: timeZoneResolver); + if (endInstant == startInstant) { + return !startInstant.isBefore(rangeStartUtc) && + startInstant.isBefore(rangeEndUtc); + } + return startInstant.isBefore(rangeEndUtc) && + endInstant.isAfter(rangeStartUtc); +} + +DateTime icalTemporalToUtc( + IcalTemporalValue value, { + IcalTimeZoneResolver? resolver, +}) => (resolver ?? IcalTimeZoneResolver.system()).toUtc(value); + +bool _afterUntil( + IcalTemporalValue value, + IcalTemporalValue until, + IcalTimeZoneResolver timeZoneResolver, +) { + if (until.kind == IcalTemporalKind.date) { + return value.localValue.isAfter(until.localValue); + } + return icalTemporalToUtc( + value, + resolver: timeZoneResolver, + ).isAfter(icalTemporalToUtc(until, resolver: timeZoneResolver)); +} + +List _integerList( + String? source, { + required int minimum, + required int maximum, + required int maximumValues, + bool disallowZero = false, +}) { + if (source == null) return const []; + final segments = source.split(','); + if (segments.isEmpty || segments.length > maximumValues) throw _invalidRule(); + final result = []; + for (final segment in segments) { + final value = int.tryParse(segment); + if (value == null || + value < minimum || + value > maximum || + (disallowZero && value == 0)) { + throw _invalidRule(); + } + result.add(value); + } + return List.unmodifiable(result); +} + +List<_ByDay> _parseByDay(String? source, int maximumValues) { + if (source == null) return const []; + final segments = source.split(','); + if (segments.isEmpty || segments.length > maximumValues) throw _invalidRule(); + return List.unmodifiable([ + for (final segment in segments) _parseOneByDay(segment), + ]); +} + +_ByDay _parseOneByDay(String source) { + final match = RegExp( + r'^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$', + ).firstMatch(source); + if (match == null) throw _invalidRule(); + final ordinal = match.group(1) == null ? null : int.parse(match.group(1)!); + if (ordinal == 0 || (ordinal != null && ordinal.abs() > 53)) { + throw _invalidRule(); + } + return _ByDay(ordinal, _weekday(match.group(2)!)); +} + +int _weekday(String source) => switch (source) { + 'MO' => DateTime.monday, + 'TU' => DateTime.tuesday, + 'WE' => DateTime.wednesday, + 'TH' => DateTime.thursday, + 'FR' => DateTime.friday, + 'SA' => DateTime.saturday, + 'SU' => DateTime.sunday, + _ => throw _invalidRule(), +}; + +int? _positiveInteger(String? source) { + if (source == null) return null; + final value = int.tryParse(source); + if (value == null || value <= 0) throw _invalidRule(); + return value; +} + +DateTime _wall([ + int year = 0, + int month = 1, + int day = 1, + int hour = 0, + int minute = 0, + int second = 0, +]) => DateTime.utc(year, month, day, hour, minute, second); + +DateTime _addDays(DateTime source, int days) => _wall( + source.year, + source.month, + source.day + days, + source.hour, + source.minute, + source.second, +); + +DateTime _addMonths(DateTime source, int months) { + final zeroBased = source.year * 12 + source.month - 1 + months; + final year = zeroBased ~/ 12; + final month = zeroBased % 12 + 1; + final day = math.min(source.day, _daysInMonth(year, month)); + return _wall(year, month, day, source.hour, source.minute, source.second); +} + +DateTime _addYears(DateTime source, int years) { + final year = source.year + years; + final day = math.min(source.day, _daysInMonth(year, source.month)); + return _wall( + year, + source.month, + day, + source.hour, + source.minute, + source.second, + ); +} + +DateTime _startOfWeek(DateTime source, int weekStart) => + _addDays(source, -((source.weekday - weekStart) % 7)); + +int _daysInMonth(int year, int month) => + _wall(year, month + 1).subtract(const Duration(days: 1)).day; + +int _daysInYear(int year) => + DateTime.utc(year + 1).difference(_wall(year)).inDays; + +({int year, int week}) _weekOfYear(DateTime date, int weekStart) { + var year = date.year; + var first = _weekOneStart(year, weekStart); + if (date.isBefore(first)) { + year -= 1; + first = _weekOneStart(year, weekStart); + } else { + final next = _weekOneStart(year + 1, weekStart); + if (!date.isBefore(next)) { + year += 1; + first = next; + } + } + return (year: year, week: date.difference(first).inDays ~/ 7 + 1); +} + +DateTime _weekOneStart(int year, int weekStart) => + _startOfWeek(_wall(year, 1, 4), weekStart); + +int _weeksInYear(int year, int weekStart) => + _weekOneStart( + year + 1, + weekStart, + ).difference(_weekOneStart(year, weekStart)).inDays ~/ + 7; + +DavException _invalidRule() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalInvalidRecurrenceRule', + safeMessage: 'An iCalendar component contained an invalid recurrence rule.', +); + +DavException _occurrenceLimitError() => _limitError( + 'IcalRecurrenceOccurrenceLimitExceeded', + 'A recurrence set produced too many occurrences.', +); + +DavException _limitError(String code, String message) => DavException( + kind: DavErrorKind.limitExceeded, + code: code, + safeMessage: message, +); diff --git a/lib/src/dav/ical/ical_semantics.dart b/lib/src/dav/ical/ical_semantics.dart new file mode 100644 index 0000000..7da67dd --- /dev/null +++ b/lib/src/dav/ical/ical_semantics.dart @@ -0,0 +1,586 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +import '../dav_errors.dart'; +import 'ical_document.dart'; + +enum IcalTemporalKind { date, floatingDateTime, utcDateTime, tzidDateTime } + +final class IcalTemporalValue { + const IcalTemporalValue({ + required this.rawValue, + required this.kind, + required this.localValue, + required this.timeZoneId, + }); + + final String rawValue; + final IcalTemporalKind kind; + final DateTime localValue; + final String? timeZoneId; + + bool get isDate => kind == IcalTemporalKind.date; + + String get recurrenceKey => switch (kind) { + IcalTemporalKind.date => 'VALUE=DATE:$rawValue', + IcalTemporalKind.tzidDateTime => 'TZID=$timeZoneId:$rawValue', + IcalTemporalKind.utcDateTime => 'UTC:$rawValue', + IcalTemporalKind.floatingDateTime => 'FLOATING:$rawValue', + }; +} + +final class IcalDuration { + const IcalDuration({ + required this.negative, + required this.weeks, + required this.days, + required this.hours, + required this.minutes, + required this.seconds, + }); + + final bool negative; + final int weeks; + final int days; + final int hours; + final int minutes; + final int seconds; + + Duration get duration { + final value = Duration( + days: weeks * 7 + days, + hours: hours, + minutes: minutes, + seconds: seconds, + ); + return negative ? -value : value; + } +} + +enum IcalTaskUiState { open, inProgress, completed, cancelled } + +final class IcalSemanticComponent { + IcalSemanticComponent._({ + required this.documentComponent, + required this.componentType, + required this.uid, + required this.recurrenceIdKey, + required this.recurrenceId, + required this.summary, + required this.description, + required this.location, + required this.url, + required this.status, + required this.classification, + required this.transparency, + required this.start, + required this.end, + required this.due, + required this.completed, + required this.duration, + required this.sequence, + required this.dtstamp, + required this.created, + required this.lastModified, + required this.priority, + required this.percentComplete, + required this.parentUid, + required this.sortOrder, + required this.categories, + required this.recurrenceRules, + required this.recurrenceDates, + required this.exceptionDates, + required this.attendees, + required this.organizers, + required this.alarms, + required this.extensionProperties, + required this.semanticHash, + }); + + factory IcalSemanticComponent.fromDocumentComponent(IcalComponent component) { + final type = component.name; + final uidProperties = component + .propertiesNamed('UID') + .toList(growable: false); + final uid = uidProperties.firstOrNull?.rawValue.trim(); + if ((type == 'VEVENT' || type == 'VTODO') && + (uid == null || uid.isEmpty || uidProperties.length != 1)) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalCalendarObjectInvariantFailed', + safeMessage: 'An event or task component must contain exactly one UID.', + ); + } + final status = _raw(component, 'STATUS')?.toUpperCase(); + final percent = _integer(component, 'PERCENT-COMPLETE'); + final completed = parseIcalTemporal(component.firstProperty('COMPLETED')); + return IcalSemanticComponent._( + documentComponent: component, + componentType: type, + uid: uid, + recurrenceIdKey: icalRecurrenceIdKey( + component.firstProperty('RECURRENCE-ID'), + ), + recurrenceId: parseIcalTemporal(component.firstProperty('RECURRENCE-ID')), + summary: _text(component, 'SUMMARY'), + description: _text(component, 'DESCRIPTION'), + location: _text(component, 'LOCATION'), + url: _raw(component, 'URL'), + status: status, + classification: _raw(component, 'CLASS'), + transparency: _raw(component, 'TRANSP'), + start: parseIcalTemporal(component.firstProperty('DTSTART')), + end: parseIcalTemporal(component.firstProperty('DTEND')), + due: parseIcalTemporal(component.firstProperty('DUE')), + completed: completed, + duration: parseIcalDuration(_raw(component, 'DURATION')), + sequence: _integer(component, 'SEQUENCE'), + dtstamp: parseIcalTemporal(component.firstProperty('DTSTAMP')), + created: parseIcalTemporal(component.firstProperty('CREATED')), + lastModified: parseIcalTemporal(component.firstProperty('LAST-MODIFIED')), + priority: _integer(component, 'PRIORITY'), + percentComplete: percent, + parentUid: _parentUid(component), + sortOrder: _integer(component, 'X-APPLE-SORT-ORDER'), + categories: _categories(component), + recurrenceRules: _rawValues(component, 'RRULE'), + recurrenceDates: _rawValues(component, 'RDATE'), + exceptionDates: _rawValues(component, 'EXDATE'), + attendees: _structuredAddresses(component, 'ATTENDEE'), + organizers: _structuredAddresses(component, 'ORGANIZER'), + alarms: component.componentsNamed('VALARM').toList(growable: false), + extensionProperties: _extensionProperties(component), + semanticHash: semanticComponentHash(component), + ); + } + + final IcalComponent documentComponent; + final String componentType; + final String? uid; + final String? recurrenceIdKey; + final IcalTemporalValue? recurrenceId; + final String? summary; + final String? description; + final String? location; + final String? url; + final String? status; + final String? classification; + final String? transparency; + final IcalTemporalValue? start; + final IcalTemporalValue? end; + final IcalTemporalValue? due; + final IcalTemporalValue? completed; + final IcalDuration? duration; + final int? sequence; + final IcalTemporalValue? dtstamp; + final IcalTemporalValue? created; + final IcalTemporalValue? lastModified; + final int? priority; + final int? percentComplete; + final String? parentUid; + final int? sortOrder; + final List categories; + final List recurrenceRules; + final List recurrenceDates; + final List exceptionDates; + final List> attendees; + final List> organizers; + final List alarms; + final Map> extensionProperties; + final String semanticHash; + + bool get isCancelled => status == 'CANCELLED'; + + IcalTaskUiState get taskUiState { + if (status == 'CANCELLED') { + return IcalTaskUiState.cancelled; + } + if (status == 'COMPLETED' || completed != null) { + return IcalTaskUiState.completed; + } + if (status == 'IN-PROCESS' || + (percentComplete != null && + percentComplete! > 0 && + percentComplete! < 100)) { + return IcalTaskUiState.inProgress; + } + return IcalTaskUiState.open; + } +} + +int nextcloudTaskSortOrder( + IcalSemanticComponent component, { + IcalSemanticComponent? fallback, +}) { + final explicit = component.sortOrder ?? fallback?.sortOrder; + if (explicit != null) return explicit; + final created = component.created ?? fallback?.created; + if (created == null) return 0; + return created.localValue.difference(DateTime.utc(2001, 1, 1)).inSeconds; +} + +final class IcalComponentIndexEntry { + const IcalComponentIndexEntry({ + required this.componentType, + required this.uid, + required this.recurrenceIdKey, + required this.sequence, + required this.dtstampUtc, + required this.lastModifiedUtc, + required this.semanticHash, + required this.parserProfileVersion, + }); + + final String componentType; + final String uid; + final String? recurrenceIdKey; + final int? sequence; + final String? dtstampUtc; + final String? lastModifiedUtc; + final String semanticHash; + final int parserProfileVersion; +} + +final class IcalSemanticDocument { + IcalSemanticDocument._({ + required this.document, + required this.components, + required this.timeZones, + required this.semanticHash, + }); + + factory IcalSemanticDocument.parse(String rawIcs) { + final document = IcalDocument.parse(rawIcs); + final semanticComponents = []; + final timeZones = []; + for (final component in document.calendarComponents) { + if (component.name == 'VEVENT' || component.name == 'VTODO') { + semanticComponents.add( + IcalSemanticComponent.fromDocumentComponent(component), + ); + } else if (component.name == 'VTIMEZONE') { + timeZones.add(component); + } + } + final componentTypes = semanticComponents + .map((component) => component.componentType) + .toSet(); + final componentUids = semanticComponents + .map((component) => component.uid) + .whereType() + .toSet(); + final nonTimeZoneComponents = document.calendarComponents + .where((component) => component.name != 'VTIMEZONE') + .toList(growable: false); + if (nonTimeZoneComponents.isEmpty || + (semanticComponents.isNotEmpty && + (componentTypes.length != 1 || componentUids.length != 1))) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalCalendarObjectInvariantFailed', + safeMessage: + 'A CalDAV object must contain one component type and one UID recurrence set.', + ); + } + return IcalSemanticDocument._( + document: document, + components: List.unmodifiable(semanticComponents), + timeZones: List.unmodifiable(timeZones), + semanticHash: semanticDocumentHash(document), + ); + } + + final IcalDocument document; + final List components; + final List timeZones; + final String semanticHash; + + /// The primary top-level component type retained by this raw resource. + /// BusyMax projects VEVENT and VTODO components, but unsupported component + /// types still remain valid raw synchronization content. + String? get dominantComponentType { + if (components.isNotEmpty) return components.first.componentType; + for (final component in document.calendarComponents) { + if (component.name != 'VTIMEZONE') return component.name; + } + return null; + } + + int get componentMask { + var mask = 0; + if (components.any((component) => component.componentType == 'VEVENT')) { + mask |= 1 << 0; + } + if (components.any((component) => component.componentType == 'VTODO')) { + mask |= 1 << 1; + } + if (timeZones.isNotEmpty) mask |= 1 << 2; + return mask; + } + + String? get primaryUid { + final uids = components + .map((component) => component.uid) + .whereType() + .toSet(); + return uids.length == 1 ? uids.single : null; + } + + List buildIndex({int profileVersion = 1}) => [ + for (final component in components) + IcalComponentIndexEntry( + componentType: component.componentType, + uid: component.uid!, + recurrenceIdKey: component.recurrenceIdKey, + sequence: component.sequence, + dtstampUtc: _utcText(component.dtstamp), + lastModifiedUtc: _utcText(component.lastModified), + semanticHash: component.semanticHash, + parserProfileVersion: profileVersion, + ), + ]; +} + +IcalTemporalValue? parseIcalTemporal(IcalProperty? property) { + if (property == null || property.rawValue.trim().isEmpty) return null; + final raw = property.rawValue.trim(); + final explicitDate = + property.parameterValue('VALUE')?.toUpperCase() == 'DATE'; + final tzid = property.parameterValue('TZID'); + if (explicitDate || RegExp(r'^[0-9]{8}$').hasMatch(raw)) { + final value = _parseDate(raw); + return IcalTemporalValue( + rawValue: raw, + kind: IcalTemporalKind.date, + localValue: value, + timeZoneId: null, + ); + } + final utc = raw.endsWith('Z'); + final value = _parseDateTime(utc ? raw.substring(0, raw.length - 1) : raw); + return IcalTemporalValue( + rawValue: raw, + kind: utc + ? IcalTemporalKind.utcDateTime + : tzid != null + ? IcalTemporalKind.tzidDateTime + : IcalTemporalKind.floatingDateTime, + localValue: utc ? value.toUtc() : value, + timeZoneId: tzid, + ); +} + +IcalDuration? parseIcalDuration(String? source) { + if (source == null || source.isEmpty) return null; + final match = RegExp( + r'^([+-])?P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$', + ).firstMatch(source); + if (match == null || + match.groups([2, 3, 4, 5, 6]).every((value) => value == null)) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalInvalidDuration', + safeMessage: 'An iCalendar component contained an invalid duration.', + ); + } + return IcalDuration( + negative: match.group(1) == '-', + weeks: int.tryParse(match.group(2) ?? '') ?? 0, + days: int.tryParse(match.group(3) ?? '') ?? 0, + hours: int.tryParse(match.group(4) ?? '') ?? 0, + minutes: int.tryParse(match.group(5) ?? '') ?? 0, + seconds: int.tryParse(match.group(6) ?? '') ?? 0, + ); +} + +String semanticDocumentHash(IcalDocument document) => + sha256.convert(utf8.encode(_canonicalComponent(document.root))).toString(); + +String semanticComponentHash(IcalComponent component) => + sha256.convert(utf8.encode(_canonicalComponent(component))).toString(); + +Set changedIcalProperties( + IcalComponent baseline, + IcalComponent current, +) { + final names = { + ...baseline.properties.map((property) => property.name), + ...current.properties.map((property) => property.name), + }; + return { + for (final name in names) + if (_canonicalProperties(baseline, name) != + _canonicalProperties(current, name)) + name, + }; +} + +String _canonicalComponent(IcalComponent component) { + final propertyLines = + component.properties.map(_canonicalProperty).toList(growable: false) + ..sort(); + final nested = + component.components.map(_canonicalComponent).toList(growable: false) + ..sort(); + return jsonEncode({ + 'type': component.name, + 'properties': propertyLines, + 'components': nested, + }); +} + +String _canonicalProperty(IcalProperty property) { + final parameters = [ + for (final parameter in property.parameters) + '${parameter.name}=${[...parameter.values]..sort()}', + ]..sort(); + return '${property.group ?? ''}.${property.name};${parameters.join(';')}:' + '${property.rawValue}'; +} + +String _canonicalProperties(IcalComponent component, String name) { + final values = + component.propertiesNamed(name).map(_canonicalProperty).toList()..sort(); + return values.join('\n'); +} + +DateTime _parseDate(String source) { + if (!RegExp(r'^[0-9]{8}$').hasMatch(source)) { + throw _invalidTemporal(); + } + return _checkedDateTime( + int.parse(source.substring(0, 4)), + int.parse(source.substring(4, 6)), + int.parse(source.substring(6, 8)), + ); +} + +DateTime _parseDateTime(String source) { + final match = RegExp( + r'^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$', + ).firstMatch(source); + if (match == null) throw _invalidTemporal(); + return _checkedDateTime( + int.parse(match.group(1)!), + int.parse(match.group(2)!), + int.parse(match.group(3)!), + int.parse(match.group(4)!), + int.parse(match.group(5)!), + int.parse(match.group(6)!), + ); +} + +DateTime _checkedDateTime( + int year, + int month, + int day, [ + int hour = 0, + int minute = 0, + int second = 0, +]) { + final value = DateTime.utc(year, month, day, hour, minute, second); + if (value.year != year || + value.month != month || + value.day != day || + value.hour != hour || + value.minute != minute || + value.second != second) { + throw _invalidTemporal(); + } + return value; +} + +DavException _invalidTemporal() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalInvalidTemporalValue', + safeMessage: 'An iCalendar component contained an invalid date or time.', +); + +String? _raw(IcalComponent component, String name) => + component.firstProperty(name)?.rawValue; + +String? _text(IcalComponent component, String name) => + component.firstProperty(name)?.decodedTextValue; + +int? _integer(IcalComponent component, String name) { + final value = _raw(component, name); + if (value == null || value.isEmpty) return null; + final parsed = int.tryParse(value); + if (parsed == null) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalInvalidIntegerValue', + safeMessage: 'An iCalendar component contained an invalid integer.', + ); + } + return parsed; +} + +List _rawValues(IcalComponent component, String name) => component + .propertiesNamed(name) + .map((property) => property.rawValue) + .toList(growable: false); + +List _categories(IcalComponent component) => [ + for (final property in component.propertiesNamed('CATEGORIES')) + ..._splitEscaped(property.rawValue, ',').map(decodeIcalText), +]; + +String? _parentUid(IcalComponent component) { + for (final relation in component.propertiesNamed('RELATED-TO')) { + final relationType = relation.parameterValue('RELTYPE')?.toUpperCase(); + if (relationType == null || relationType == 'PARENT') { + return relation.rawValue.trim().isEmpty ? null : relation.rawValue.trim(); + } + } + return null; +} + +List> _structuredAddresses( + IcalComponent component, + String propertyName, +) => [ + for (final property in component.propertiesNamed(propertyName)) + { + 'value': property.rawValue, + 'parameters': [ + for (final parameter in property.parameters) + {'name': parameter.name, 'values': parameter.values}, + ], + }, +]; + +Map> _extensionProperties(IcalComponent component) { + final result = >{}; + for (final property in component.properties) { + if (property.name.startsWith('X-')) { + result.putIfAbsent(property.name, () => []).add(property.rawValue); + } + } + return result; +} + +List _splitEscaped(String source, String separator) { + final result = []; + var start = 0; + var escaped = false; + for (var index = 0; index < source.length; index += 1) { + final value = source[index]; + if (escaped) { + escaped = false; + } else if (value == r'\') { + escaped = true; + } else if (value == separator) { + result.add(source.substring(start, index)); + start = index + 1; + } + } + result.add(source.substring(start)); + return result; +} + +String? _utcText(IcalTemporalValue? value) { + if (value == null || value.kind != IcalTemporalKind.utcDateTime) return null; + return value.localValue.toUtc().toIso8601String(); +} diff --git a/lib/src/dav/ical/ical_task_alarm.dart b/lib/src/dav/ical/ical_task_alarm.dart new file mode 100644 index 0000000..069eece --- /dev/null +++ b/lib/src/dav/ical/ical_task_alarm.dart @@ -0,0 +1,400 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; + +import '../dav_errors.dart'; +import 'ical_document.dart'; +import 'ical_semantics.dart'; + +/// A lossless projection of one RFC 5545 VALARM attached to a VTODO. +/// +/// Unknown properties are retained when a supported trigger is edited. This +/// is important for alarms created by another CalDAV client. +final class IcalTaskAlarm { + IcalTaskAlarm._(this._properties); + + factory IcalTaskAlarm.fromComponent(IcalComponent component) { + if (component.name != 'VALARM') throw _invalidAlarm(); + return IcalTaskAlarm._([ + for (final property in component.properties) + _AlarmProperty( + name: property.name, + value: property.rawValue, + parameters: [ + for (final parameter in property.parameters) + IcalParameter( + name: parameter.name, + values: List.unmodifiable(parameter.values), + wasQuoted: parameter.wasQuoted, + ), + ], + ), + ]); + } + + factory IcalTaskAlarm.fromJson(Map json) { + final values = json['properties']; + if (values is! List) throw _invalidAlarm(); + final properties = <_AlarmProperty>[]; + for (final value in values) { + if (value is! Map) throw _invalidAlarm(); + final map = value.cast(); + final name = map['name']?.toString().trim().toUpperCase(); + final rawValue = map['value']; + if (name == null || + name.isEmpty || + rawValue is! String || + !_propertyName.hasMatch(name)) { + throw _invalidAlarm(); + } + final parameters = []; + final rawParameters = map['parameters']; + if (rawParameters != null) { + if (rawParameters is! List) throw _invalidAlarm(); + for (final rawParameter in rawParameters) { + if (rawParameter is! Map) throw _invalidAlarm(); + final parameter = rawParameter.cast(); + final parameterName = parameter['name'] + ?.toString() + .trim() + .toUpperCase(); + final parameterValues = parameter['values']; + if (parameterName == null || + !_propertyName.hasMatch(parameterName) || + parameterValues is! List) { + throw _invalidAlarm(); + } + parameters.add( + IcalParameter( + name: parameterName, + values: [for (final item in parameterValues) item.toString()], + wasQuoted: parameter['wasQuoted'] == true, + ), + ); + } + } + properties.add( + _AlarmProperty( + name: name, + value: rawValue, + parameters: List.unmodifiable(parameters), + ), + ); + } + return IcalTaskAlarm._(List.unmodifiable(properties)); + } + + factory IcalTaskAlarm.displayAbsolute( + DateTime value, { + String description = 'This is a todo reminder.', + }) => IcalTaskAlarm._([ + const _AlarmProperty(name: 'ACTION', value: 'DISPLAY'), + _AlarmProperty(name: 'DESCRIPTION', value: encodeIcalText(description)), + _AlarmProperty( + name: 'TRIGGER', + value: _utcIcal(value), + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE-TIME'], wasQuoted: false), + ], + ), + ]); + + factory IcalTaskAlarm.displayRelative( + Duration offset, { + required bool relatedToDue, + String description = 'This is a todo reminder.', + }) => IcalTaskAlarm._([ + const _AlarmProperty(name: 'ACTION', value: 'DISPLAY'), + _AlarmProperty(name: 'DESCRIPTION', value: encodeIcalText(description)), + _AlarmProperty( + name: 'TRIGGER', + value: _duration(offset), + parameters: [ + IcalParameter( + name: 'RELATED', + values: [relatedToDue ? 'END' : 'START'], + wasQuoted: false, + ), + ], + ), + ]); + + final List<_AlarmProperty> _properties; + + String get action => _first('ACTION')?.value.toUpperCase() ?? ''; + String? get description => _first('DESCRIPTION')?.value; + String get triggerRaw => _first('TRIGGER')?.value ?? ''; + + bool get _hasSingleTrigger => + _properties.where((property) => property.name == 'TRIGGER').length == 1; + + bool get isAbsolute => + _hasSingleTrigger && + (_first('TRIGGER')!.parameter('VALUE')?.toUpperCase() == 'DATE-TIME' || + RegExp(r'^\d{8}T\d{6}Z$').hasMatch(triggerRaw)); + + bool get isRelative { + if (!_hasSingleTrigger || isAbsolute) return false; + try { + return parseIcalDuration(triggerRaw) != null; + } on DavException { + return false; + } + } + + bool get isRelatedToDue => + _hasSingleTrigger && + _first('TRIGGER')!.parameter('RELATED')?.toUpperCase() == 'END'; + + Duration? get relativeOffset { + if (!isRelative) return null; + return parseIcalDuration(triggerRaw)?.duration; + } + + DateTime? get absoluteUtc { + if (!isAbsolute || !RegExp(r'^\d{8}T\d{6}Z$').hasMatch(triggerRaw)) { + return null; + } + final value = triggerRaw; + return DateTime.utc( + int.parse(value.substring(0, 4)), + int.parse(value.substring(4, 6)), + int.parse(value.substring(6, 8)), + int.parse(value.substring(9, 11)), + int.parse(value.substring(11, 13)), + int.parse(value.substring(13, 15)), + ); + } + + bool get canEditTrigger => isRelative || absoluteUtc != null; + + bool canEditTriggerFor({required bool allDay}) { + if (absoluteUtc != null) return true; + final offset = relativeOffset; + if (offset == null || isRelatedToDue) return false; + if (!allDay && offset > Duration.zero) return false; + if (allDay && offset > const Duration(days: 1)) return false; + return true; + } + + IcalTaskAlarm withAbsoluteTrigger(DateTime value) => + _withTrigger(_utcIcal(value), const [ + IcalParameter(name: 'VALUE', values: ['DATE-TIME'], wasQuoted: false), + ]); + + IcalTaskAlarm withRelativeTrigger( + Duration offset, { + required bool relatedToDue, + }) => _withTrigger(_duration(offset), [ + IcalParameter( + name: 'RELATED', + values: [relatedToDue ? 'END' : 'START'], + wasQuoted: false, + ), + ]); + + IcalTaskAlarm _withTrigger(String value, List parameters) { + final result = <_AlarmProperty>[]; + for (final property in _properties) { + result.add( + property.name == 'TRIGGER' + ? _AlarmProperty( + name: 'TRIGGER', + value: value, + parameters: List.unmodifiable(parameters), + ) + : property, + ); + } + return IcalTaskAlarm._(List.unmodifiable(result)); + } + + IcalComponent toComponent() => IcalComponent( + name: 'VALARM', + children: [ + for (final property in _properties) + IcalProperty( + group: null, + name: property.name, + parameters: [ + for (final parameter in property.parameters) + IcalParameter( + name: parameter.name, + values: List.of(parameter.values), + wasQuoted: parameter.wasQuoted, + ), + ], + rawValue: property.value, + originalPhysicalLines: const [], + isDirty: true, + ), + ], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + structurallyDirty: true, + ); + + Map toJson() => { + 'properties': [ + for (final property in _properties) + { + 'name': property.name, + 'value': property.value, + if (property.parameters.isNotEmpty) + 'parameters': [ + for (final parameter in property.parameters) + { + 'name': parameter.name, + 'values': parameter.values, + if (parameter.wasQuoted) 'wasQuoted': true, + }, + ], + }, + ], + }; + + @override + bool operator ==(Object other) => + other is IcalTaskAlarm && + const DeepCollectionEquality().equals(toJson(), other.toJson()); + + @override + int get hashCode => const DeepCollectionEquality().hash(toJson()); + + _AlarmProperty? _first(String name) => + _properties.firstWhereOrNull((property) => property.name == name); +} + +enum IcalAllDayAlarmUnit { days, weeks } + +/// Editable representation of a relative alarm on an all-day task. +/// +/// Nextcloud presents these alarms as a number of days or weeks before the +/// task, followed by a wall-clock time. The corresponding VALARM trigger is a +/// signed duration relative to the task's midnight boundary. +final class IcalAllDayAlarmOffset { + const IcalAllDayAlarmOffset({ + required this.amount, + required this.unit, + required this.hour, + required this.minute, + }); + + factory IcalAllDayAlarmOffset.fromDuration(Duration offset) { + final signedSeconds = offset.inSeconds; + final isBefore = signedSeconds < 0; + final absoluteSeconds = signedSeconds.abs(); + final wholeDays = isBefore + ? (absoluteSeconds + Duration.secondsPerDay - 1) ~/ + Duration.secondsPerDay + : absoluteSeconds ~/ Duration.secondsPerDay; + final timeSeconds = isBefore + ? wholeDays * Duration.secondsPerDay - absoluteSeconds + : absoluteSeconds % Duration.secondsPerDay; + final useWeeks = wholeDays != 0 && wholeDays % 7 == 0; + + return IcalAllDayAlarmOffset( + amount: useWeeks ? wholeDays ~/ 7 : wholeDays, + unit: useWeeks ? IcalAllDayAlarmUnit.weeks : IcalAllDayAlarmUnit.days, + hour: timeSeconds ~/ Duration.secondsPerHour, + minute: + (timeSeconds % Duration.secondsPerHour) ~/ Duration.secondsPerMinute, + ); + } + + final int amount; + final IcalAllDayAlarmUnit unit; + final int hour; + final int minute; + + Duration toDuration() { + if (amount < 0 || hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw ArgumentError('Invalid all-day reminder offset.'); + } + final days = amount * (unit == IcalAllDayAlarmUnit.weeks ? 7 : 1); + final timeSeconds = + hour * Duration.secondsPerHour + minute * Duration.secondsPerMinute; + return Duration( + seconds: days == 0 + ? timeSeconds + : timeSeconds - days * Duration.secondsPerDay, + ); + } +} + +List decodeIcalTaskAlarms(String? source) { + if (source == null || source.isEmpty) return const []; + try { + final decoded = jsonDecode(source); + if (decoded is! List) throw _invalidAlarm(); + return List.unmodifiable([ + for (final item in decoded) + IcalTaskAlarm.fromJson((item as Map).cast()), + ]); + } on DavException { + rethrow; + } on Object { + throw _invalidAlarm(); + } +} + +String encodeIcalTaskAlarms(List alarms) => + jsonEncode([for (final alarm in alarms) alarm.toJson()]); + +final class _AlarmProperty { + const _AlarmProperty({ + required this.name, + required this.value, + this.parameters = const [], + }); + + final String name; + final String value; + final List parameters; + + String? parameter(String name) { + final upper = name.toUpperCase(); + return parameters + .firstWhereOrNull((parameter) => parameter.name == upper) + ?.values + .firstOrNull; + } +} + +final _propertyName = RegExp(r'^[A-Z0-9-]+$'); + +String _duration(Duration value) { + var seconds = value.inSeconds; + final negative = seconds < 0; + seconds = seconds.abs(); + final days = seconds ~/ Duration.secondsPerDay; + seconds %= Duration.secondsPerDay; + final hours = seconds ~/ Duration.secondsPerHour; + seconds %= Duration.secondsPerHour; + final minutes = seconds ~/ Duration.secondsPerMinute; + seconds %= Duration.secondsPerMinute; + final buffer = StringBuffer(negative ? '-P' : 'P'); + if (days != 0) buffer.write('${days}D'); + if (hours != 0 || minutes != 0 || seconds != 0 || days == 0) { + buffer.write('T'); + if (hours != 0) buffer.write('${hours}H'); + if (minutes != 0) buffer.write('${minutes}M'); + if (seconds != 0 || (hours == 0 && minutes == 0)) { + buffer.write('${seconds}S'); + } + } + return buffer.toString(); +} + +String _utcIcal(DateTime value) { + final utc = value.toUtc(); + String two(int number) => number.toString().padLeft(2, '0'); + return '${utc.year.toString().padLeft(4, '0')}${two(utc.month)}' + '${two(utc.day)}T${two(utc.hour)}${two(utc.minute)}${two(utc.second)}Z'; +} + +DavException _invalidAlarm() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'IcalTaskAlarmInvalid', + safeMessage: 'A task reminder was invalid.', +); diff --git a/lib/src/dav/ical/ical_task_recurrence.dart b/lib/src/dav/ical/ical_task_recurrence.dart new file mode 100644 index 0000000..737bb6d --- /dev/null +++ b/lib/src/dav/ical/ical_task_recurrence.dart @@ -0,0 +1,485 @@ +import 'dart:convert'; + +enum IcalTaskRecurrenceFrequency { none, daily, weekly, monthly, yearly } + +/// The recurrence subset exposed by Nextcloud Tasks 0.18.x. +/// +/// Rules outside that subset remain available through [rawRules] and are not +/// rewritten. Supported rules cover frequency, interval, BYDAY, BYMONTH, +/// BYMONTHDAY, BYSETPOS, COUNT, and UNTIL. +final class IcalTaskRecurrence { + const IcalTaskRecurrence({ + required this.frequency, + required this.interval, + required this.byDay, + required this.byMonth, + required this.byMonthDay, + required this.bySetPosition, + required this.count, + required this.untilRaw, + required this.recurrenceDates, + required this.exceptionDates, + required this.rawRules, + required this.isSupported, + }); + + const IcalTaskRecurrence.none() + : frequency = IcalTaskRecurrenceFrequency.none, + interval = 1, + byDay = const [], + byMonth = const [], + byMonthDay = const [], + bySetPosition = null, + count = null, + untilRaw = null, + recurrenceDates = const [], + exceptionDates = const [], + rawRules = const [], + isSupported = true; + + factory IcalTaskRecurrence.fromJson(String? source, {DateTime? baseDate}) { + if (source == null || source.isEmpty) { + return const IcalTaskRecurrence.none(); + } + try { + final decoded = jsonDecode(source); + if (decoded is! Map) return _unsupported(const [], const [], const []); + final map = decoded.cast(); + final rules = _strings(map['rules']); + final dates = _strings(map['dates']); + final excludedDates = _strings(map['excludedDates']); + if (rules.isEmpty) { + return IcalTaskRecurrence( + frequency: IcalTaskRecurrenceFrequency.none, + interval: 1, + byDay: const [], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + count: null, + untilRaw: null, + recurrenceDates: dates, + exceptionDates: excludedDates, + rawRules: rules, + isSupported: true, + ); + } + if (rules.length != 1) return _unsupported(rules, dates, excludedDates); + final parts = {}; + for (final segment in rules.single.split(';')) { + final separator = segment.indexOf('='); + if (separator <= 0 || separator == segment.length - 1) { + return _unsupported(rules, dates, excludedDates); + } + final key = segment.substring(0, separator).toUpperCase(); + if (parts.containsKey(key) || !_supportedParts.contains(key)) { + return _unsupported(rules, dates, excludedDates); + } + parts[key] = segment.substring(separator + 1).toUpperCase(); + } + final frequency = switch (parts['FREQ']) { + 'DAILY' => IcalTaskRecurrenceFrequency.daily, + 'WEEKLY' => IcalTaskRecurrenceFrequency.weekly, + 'MONTHLY' => IcalTaskRecurrenceFrequency.monthly, + 'YEARLY' => IcalTaskRecurrenceFrequency.yearly, + _ => null, + }; + final interval = int.tryParse(parts['INTERVAL'] ?? '1'); + final count = parts['COUNT'] == null + ? null + : int.tryParse(parts['COUNT']!); + final byDay = _csv(parts['BYDAY']); + final byMonth = _integers(parts['BYMONTH']); + final byMonthDay = _integers(parts['BYMONTHDAY']); + final bySetPositions = _integers(parts['BYSETPOS']); + final until = parts['UNTIL']; + final valid = + frequency != null && + interval != null && + interval >= 1 && + interval <= 366 && + (count == null || (count >= 1 && count <= 3500)) && + !(count != null && until != null) && + (until == null || _validUntil(until)) && + byMonth.every((value) => value >= 1 && value <= 12) && + byMonthDay.every( + (value) => value != 0 && value >= -31 && value <= 31, + ) && + bySetPositions.length <= 1 && + bySetPositions.every( + (value) => value != 0 && value >= -366 && value <= 366, + ); + if (!valid) return _unsupported(rules, dates, excludedDates); + final supportedFrequency = frequency; + final supportedInterval = interval; + + final normalized = _normalizeEditorRule( + frequency: supportedFrequency, + byDay: byDay, + byMonth: byMonth, + byMonthDay: byMonthDay, + bySetPositions: bySetPositions, + baseDate: baseDate, + ); + if (normalized == null) { + return _unsupported(rules, dates, excludedDates); + } + return IcalTaskRecurrence( + frequency: supportedFrequency, + interval: supportedInterval, + byDay: List.unmodifiable(normalized.byDay), + byMonth: List.unmodifiable(normalized.byMonth), + byMonthDay: List.unmodifiable(normalized.byMonthDay), + bySetPosition: normalized.bySetPosition, + count: count, + untilRaw: until, + recurrenceDates: List.unmodifiable(dates), + exceptionDates: List.unmodifiable(excludedDates), + rawRules: List.unmodifiable(rules), + isSupported: true, + ); + } on Object { + return _unsupported(const [], const [], const []); + } + } + + final IcalTaskRecurrenceFrequency frequency; + final int interval; + final List byDay; + final List byMonth; + final List byMonthDay; + final int? bySetPosition; + final int? count; + final String? untilRaw; + final List recurrenceDates; + final List exceptionDates; + final List rawRules; + final bool isSupported; + + bool get repeats => frequency != IcalTaskRecurrenceFrequency.none; + + String? get untilDate { + final value = untilRaw; + if (value == null || value.length < 8) return null; + final parsed = _parseUntil(value); + if (parsed == null) return null; + final display = value.endsWith('Z') ? parsed.toLocal() : parsed; + return '${display.year.toString().padLeft(4, '0')}-' + '${display.month.toString().padLeft(2, '0')}-' + '${display.day.toString().padLeft(2, '0')}'; + } + + IcalTaskRecurrence copyWith({ + IcalTaskRecurrenceFrequency? frequency, + int? interval, + List? byDay, + List? byMonth, + List? byMonthDay, + Object? bySetPosition = _unchanged, + Object? count = _unchanged, + Object? untilRaw = _unchanged, + List? recurrenceDates, + List? exceptionDates, + }) => IcalTaskRecurrence( + frequency: frequency ?? this.frequency, + interval: interval ?? this.interval, + byDay: List.unmodifiable(byDay ?? this.byDay), + byMonth: List.unmodifiable(byMonth ?? this.byMonth), + byMonthDay: List.unmodifiable(byMonthDay ?? this.byMonthDay), + bySetPosition: bySetPosition == _unchanged + ? this.bySetPosition + : bySetPosition as int?, + count: count == _unchanged ? this.count : count as int?, + untilRaw: untilRaw == _unchanged ? this.untilRaw : untilRaw as String?, + recurrenceDates: List.unmodifiable(recurrenceDates ?? this.recurrenceDates), + exceptionDates: List.unmodifiable(exceptionDates ?? this.exceptionDates), + rawRules: const [], + isSupported: true, + ); + + IcalTaskRecurrence withUntilDate(String? date, {required bool allDay}) { + if (date == null || date.isEmpty) return copyWith(untilRaw: null); + final parsed = DateTime.tryParse(date); + if (parsed == null) return this; + final localDate = DateTime(parsed.year, parsed.month, parsed.day); + final basic = _basicDate(localDate); + if (allDay) return copyWith(untilRaw: basic); + final utc = localDate.toUtc(); + return copyWith( + untilRaw: + '${_basicDate(utc)}T' + '${utc.hour.toString().padLeft(2, '0')}' + '${utc.minute.toString().padLeft(2, '0')}' + '${utc.second.toString().padLeft(2, '0')}Z', + ); + } + + String toJsonString() => jsonEncode({ + 'rules': repeats ? [toRrule()] : const [], + 'dates': recurrenceDates, + 'excludedDates': exceptionDates, + }); + + String toRrule() { + if (!isSupported || !repeats) { + throw StateError('Only a supported repeating rule can be serialized.'); + } + final result = [ + 'FREQ=${frequency.name.toUpperCase()}', + 'INTERVAL=$interval', + if (byDay.isNotEmpty) 'BYDAY=${byDay.join(',')}', + if (byMonth.isNotEmpty) 'BYMONTH=${byMonth.join(',')}', + if (byMonthDay.isNotEmpty) 'BYMONTHDAY=${byMonthDay.join(',')}', + if (bySetPosition != null) 'BYSETPOS=$bySetPosition', + if (count != null) 'COUNT=$count', + if (untilRaw != null) 'UNTIL=$untilRaw', + ]; + return result.join(';'); + } +} + +const _supportedParts = { + 'FREQ', + 'INTERVAL', + 'BYDAY', + 'BYMONTH', + 'BYMONTHDAY', + 'BYSETPOS', + 'COUNT', + 'UNTIL', +}; + +const _unchanged = Object(); + +IcalTaskRecurrence _unsupported( + List rules, + List dates, + List excludedDates, +) => IcalTaskRecurrence( + frequency: IcalTaskRecurrenceFrequency.none, + interval: 1, + byDay: const [], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + count: null, + untilRaw: null, + recurrenceDates: List.unmodifiable(dates), + exceptionDates: List.unmodifiable(excludedDates), + rawRules: List.unmodifiable(rules), + isSupported: false, +); + +List _strings(Object? source) { + if (source == null) return const []; + if (source is! List) throw const FormatException(); + return [for (final value in source) value.toString()]; +} + +List _csv(String? source) => source == null || source.isEmpty + ? const [] + : source.split(',').where((value) => value.isNotEmpty).toList(); + +List _integers(String? source) { + final values = _csv(source); + final result = []; + for (final value in values) { + final parsed = int.tryParse(value); + if (parsed == null) throw const FormatException(); + result.add(parsed); + } + return result; +} + +typedef _NormalizedEditorRule = ({ + List byDay, + List byMonth, + List byMonthDay, + int? bySetPosition, +}); + +_NormalizedEditorRule? _normalizeEditorRule({ + required IcalTaskRecurrenceFrequency frequency, + required List byDay, + required List byMonth, + required List byMonthDay, + required List bySetPositions, + required DateTime? baseDate, +}) { + switch (frequency) { + case IcalTaskRecurrenceFrequency.none: + return null; + case IcalTaskRecurrenceFrequency.daily: + if (byDay.isNotEmpty || + byMonth.isNotEmpty || + byMonthDay.isNotEmpty || + bySetPositions.isNotEmpty) { + return null; + } + return ( + byDay: const [], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + ); + case IcalTaskRecurrenceFrequency.weekly: + if (byMonth.isNotEmpty || + byMonthDay.isNotEmpty || + bySetPositions.isNotEmpty || + byDay.any((value) => !_plainWeekdays.contains(value))) { + return null; + } + return ( + byDay: byDay.isEmpty && baseDate != null + ? [_weekdayFor(baseDate.weekday)] + : byDay, + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + ); + case IcalTaskRecurrenceFrequency.monthly: + if (byMonth.isNotEmpty) return null; + return _normalizeMonthlyOrYearly( + byDay: byDay, + byMonth: const [], + byMonthDay: byMonthDay, + bySetPositions: bySetPositions, + defaultMonthDay: baseDate?.day, + ); + case IcalTaskRecurrenceFrequency.yearly: + final months = byMonth.isEmpty && baseDate != null + ? [baseDate.month] + : byMonth; + if (months.isEmpty) return null; + return _normalizeMonthlyOrYearly( + byDay: byDay, + byMonth: months, + byMonthDay: byMonthDay, + bySetPositions: bySetPositions, + defaultMonthDay: baseDate?.day, + ); + } +} + +_NormalizedEditorRule? _normalizeMonthlyOrYearly({ + required List byDay, + required List byMonth, + required List byMonthDay, + required List bySetPositions, + required int? defaultMonthDay, +}) { + if (byMonthDay.isNotEmpty) { + if (byDay.isNotEmpty || + bySetPositions.isNotEmpty || + byMonthDay.any((value) => value < 1 || value > 31)) { + return null; + } + return ( + byDay: const [], + byMonth: byMonth, + byMonthDay: byMonthDay, + bySetPosition: null, + ); + } + + if (byDay.length == 1 && bySetPositions.isEmpty) { + final ordinal = RegExp( + r'^(-?[1-5])(MO|TU|WE|TH|FR|SA|SU)$', + ).firstMatch(byDay.single); + if (ordinal != null) { + final position = int.parse(ordinal.group(1)!); + if (_editorSetPositions.contains(position)) { + return ( + byDay: [ordinal.group(2)!], + byMonth: byMonth, + byMonthDay: const [], + bySetPosition: position, + ); + } + } + } + + if (byDay.isNotEmpty && + bySetPositions.length == 1 && + _allowedEditorByDay(byDay) && + _editorSetPositions.contains(bySetPositions.single)) { + return ( + byDay: byDay, + byMonth: byMonth, + byMonthDay: const [], + bySetPosition: bySetPositions.single, + ); + } + + if (byDay.isEmpty && bySetPositions.isEmpty && defaultMonthDay != null) { + return ( + byDay: const [], + byMonth: byMonth, + byMonthDay: [defaultMonthDay], + bySetPosition: null, + ); + } + return null; +} + +bool _allowedEditorByDay(List values) { + final sorted = [...values]..sort(); + return _editorDaySets.any((allowed) { + final candidate = [...allowed]..sort(); + if (candidate.length != sorted.length) return false; + for (var index = 0; index < sorted.length; index += 1) { + if (candidate[index] != sorted[index]) return false; + } + return true; + }); +} + +bool _validUntil(String value) => _parseUntil(value) != null; + +DateTime? _parseUntil(String value) { + final match = RegExp( + r'^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z)?)?$', + ).firstMatch(value); + if (match == null) return null; + final year = int.parse(match.group(1)!); + final month = int.parse(match.group(2)!); + final day = int.parse(match.group(3)!); + final hasTime = match.group(4) != null; + final hour = hasTime ? int.parse(match.group(4)!) : 0; + final minute = hasTime ? int.parse(match.group(5)!) : 0; + final second = hasTime ? int.parse(match.group(6)!) : 0; + final parsed = match.group(7) == 'Z' + ? DateTime.utc(year, month, day, hour, minute, second) + : DateTime(year, month, day, hour, minute, second); + if (parsed.year != year || + parsed.month != month || + parsed.day != day || + parsed.hour != hour || + parsed.minute != minute || + parsed.second != second) { + return null; + } + return parsed; +} + +String _basicDate(DateTime value) => + '${value.year.toString().padLeft(4, '0')}' + '${value.month.toString().padLeft(2, '0')}' + '${value.day.toString().padLeft(2, '0')}'; + +String _weekdayFor(int weekday) => _plainWeekdays[(weekday - 1).clamp(0, 6)]; + +const _plainWeekdays = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']; +const _editorSetPositions = {1, 2, 3, 4, 5, -2, -1}; +const _editorDaySets = >[ + ['MO'], + ['TU'], + ['WE'], + ['TH'], + ['FR'], + ['SA'], + ['SU'], + ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'], + ['MO', 'TU', 'WE', 'TH', 'FR'], + ['SA', 'SU'], +]; diff --git a/lib/src/dav/ical/ical_timezone.dart b/lib/src/dav/ical/ical_timezone.dart new file mode 100644 index 0000000..170131f --- /dev/null +++ b/lib/src/dav/ical/ical_timezone.dart @@ -0,0 +1,530 @@ +import 'package:timezone/data/latest_all.dart' as time_zone_data; +import 'package:timezone/timezone.dart' as time_zone; + +import '../dav_errors.dart'; +import 'ical_document.dart'; +import 'ical_semantics.dart'; + +/// Resolves native iCalendar temporal values without flattening their wire +/// representation. Embedded VTIMEZONE definitions take precedence over the +/// bundled IANA database so provider-supplied custom zone identifiers and +/// historical rules remain meaningful during occurrence projection. +final class IcalTimeZoneResolver { + IcalTimeZoneResolver._(this._zones) { + time_zone_data.initializeTimeZones(); + } + + factory IcalTimeZoneResolver.fromDocument(IcalSemanticDocument document) { + final zones = {}; + for (final component in document.timeZones) { + final definition = _TimeZoneDefinition.parse(component); + if (zones.containsKey(definition.id)) { + throw _invalidTimeZone('IcalDuplicateTimeZone'); + } + zones[definition.id] = definition; + } + return IcalTimeZoneResolver._(Map.unmodifiable(zones)); + } + + factory IcalTimeZoneResolver.system() => + IcalTimeZoneResolver._(const {}); + + final Map _zones; + + DateTime toUtc(IcalTemporalValue value) { + if (value.kind == IcalTemporalKind.utcDateTime) { + return value.localValue.toUtc(); + } + if (value.kind == IcalTemporalKind.tzidDateTime) { + final id = value.timeZoneId; + if (id == null || id.isEmpty) throw _invalidTimeZone(); + final embedded = _zones[id]; + if (embedded != null) { + return embedded.toUtc(value.localValue); + } + try { + final location = time_zone.getLocation(id); + final wall = value.localValue; + return time_zone.TZDateTime( + location, + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ).toUtc(); + } on time_zone.LocationNotFoundException { + throw const DavException( + kind: DavErrorKind.unsupportedComponent, + code: 'IcalUnknownTimeZone', + safeMessage: + 'An iCalendar value used a time zone that could not be resolved.', + ); + } + } + final wall = value.localValue; + return DateTime.utc( + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ); + } +} + +final class _TimeZoneDefinition { + const _TimeZoneDefinition({required this.id, required this.observances}); + + factory _TimeZoneDefinition.parse(IcalComponent component) { + final id = component.firstProperty('TZID')?.decodedTextValue.trim(); + if (id == null || id.isEmpty) throw _invalidTimeZone(); + final observances = <_TimeZoneObservance>[]; + for (final child in component.components) { + if (child.name != 'STANDARD' && child.name != 'DAYLIGHT') continue; + observances.add(_TimeZoneObservance.parse(child)); + } + if (observances.isEmpty) throw _invalidTimeZone(); + return _TimeZoneDefinition( + id: id, + observances: List.unmodifiable(observances), + ); + } + + final String id; + final List<_TimeZoneObservance> observances; + + DateTime toUtc(DateTime wall) { + final transitions = <_TimeZoneTransition>[]; + for (final observance in observances) { + transitions.addAll(observance.transitionsThrough(wall.year + 1)); + } + if (transitions.isEmpty) throw _invalidTimeZone(); + transitions.sort((left, right) => left.localAt.compareTo(right.localAt)); + _TimeZoneTransition? effective; + for (final transition in transitions) { + if (transition.localAt.isAfter(wall)) break; + effective = transition; + } + final offset = effective?.offsetTo ?? transitions.first.offsetFrom; + return DateTime.utc( + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ).subtract(offset); + } +} + +final class _TimeZoneObservance { + const _TimeZoneObservance({ + required this.start, + required this.offsetFrom, + required this.offsetTo, + required this.rule, + required this.additionalDates, + }); + + factory _TimeZoneObservance.parse(IcalComponent component) { + final startProperty = component.firstProperty('DTSTART'); + final from = component.firstProperty('TZOFFSETFROM')?.rawValue.trim(); + final to = component.firstProperty('TZOFFSETTO')?.rawValue.trim(); + if (startProperty == null || from == null || to == null) { + throw _invalidTimeZone(); + } + final temporal = parseIcalTemporal(startProperty); + if (temporal == null || + temporal.kind != IcalTemporalKind.floatingDateTime) { + throw _invalidTimeZone(); + } + final rules = component.propertiesNamed('RRULE').toList(growable: false); + if (rules.length > 1) throw _invalidTimeZone(); + final dates = []; + for (final property in component.propertiesNamed('RDATE')) { + for (final token in property.rawValue.split(',')) { + if (token.contains('/')) throw _unsupportedTimeZoneRule(); + final parsed = parseIcalTemporal( + IcalProperty( + group: null, + name: 'RDATE', + parameters: property.parameters, + rawValue: token.trim(), + originalPhysicalLines: const [], + ), + ); + if (parsed == null || + parsed.kind != IcalTemporalKind.floatingDateTime) { + throw _invalidTimeZone(); + } + dates.add(parsed.localValue); + } + } + return _TimeZoneObservance( + start: temporal.localValue, + offsetFrom: _parseUtcOffset(from), + offsetTo: _parseUtcOffset(to), + rule: rules.isEmpty ? null : _TimeZoneYearlyRule.parse(rules.single), + additionalDates: List.unmodifiable(dates), + ); + } + + final DateTime start; + final Duration offsetFrom; + final Duration offsetTo; + final _TimeZoneYearlyRule? rule; + final List additionalDates; + + List<_TimeZoneTransition> transitionsThrough(int lastYear) { + final values = {start, ...additionalDates}; + final yearlyRule = rule; + if (yearlyRule != null) { + if (lastYear - start.year > 1000) { + throw const DavException( + kind: DavErrorKind.limitExceeded, + code: 'IcalTimeZoneTransitionLimitExceeded', + safeMessage: + 'An embedded time zone exceeded the safe transition limit.', + ); + } + var emitted = 0; + var stop = false; + for (var year = start.year; year <= lastYear && !stop; year += 1) { + if ((year - start.year) % yearlyRule.interval != 0) continue; + for (final candidate in yearlyRule.candidates(start, year)) { + if (candidate.isBefore(start)) continue; + if (yearlyRule.isAfterUntil(candidate, offsetFrom)) { + stop = true; + break; + } + emitted += 1; + if (yearlyRule.count != null && emitted > yearlyRule.count!) { + stop = true; + break; + } + values.add(candidate); + if (values.length > 4096) { + throw const DavException( + kind: DavErrorKind.limitExceeded, + code: 'IcalTimeZoneTransitionLimitExceeded', + safeMessage: + 'An embedded time zone exceeded the safe transition limit.', + ); + } + } + } + } + return [ + for (final value in values) + if (value.year <= lastYear) + _TimeZoneTransition( + localAt: value, + offsetFrom: offsetFrom, + offsetTo: offsetTo, + ), + ]; + } +} + +final class _TimeZoneTransition { + const _TimeZoneTransition({ + required this.localAt, + required this.offsetFrom, + required this.offsetTo, + }); + + final DateTime localAt; + final Duration offsetFrom; + final Duration offsetTo; +} + +final class _TimeZoneYearlyRule { + const _TimeZoneYearlyRule({ + required this.interval, + required this.count, + required this.until, + required this.untilIsUtc, + required this.months, + required this.monthDays, + required this.weekDays, + required this.hours, + required this.minutes, + required this.seconds, + required this.setPositions, + }); + + factory _TimeZoneYearlyRule.parse(IcalProperty property) { + final fields = {}; + for (final segment in property.rawValue.split(';')) { + final separator = segment.indexOf('='); + if (separator <= 0 || separator == segment.length - 1) { + throw _invalidTimeZone(); + } + final name = segment.substring(0, separator).toUpperCase(); + if (fields.containsKey(name)) throw _invalidTimeZone(); + fields[name] = segment.substring(separator + 1).toUpperCase(); + } + const supported = { + 'FREQ', + 'UNTIL', + 'COUNT', + 'INTERVAL', + 'BYMONTH', + 'BYMONTHDAY', + 'BYDAY', + 'BYHOUR', + 'BYMINUTE', + 'BYSECOND', + 'BYSETPOS', + 'WKST', + }; + if (fields.keys.any((key) => !supported.contains(key)) || + fields['FREQ'] != 'YEARLY') { + throw _unsupportedTimeZoneRule(); + } + final untilSource = fields['UNTIL']; + final until = untilSource == null + ? null + : _parseRuleDateTime(untilSource.replaceFirst(RegExp(r'Z$'), '')); + return _TimeZoneYearlyRule( + interval: _positive(fields['INTERVAL']) ?? 1, + count: _positive(fields['COUNT']), + until: until, + untilIsUtc: untilSource?.endsWith('Z') ?? false, + months: _integers(fields['BYMONTH'], 1, 12), + monthDays: _integers(fields['BYMONTHDAY'], -31, 31, noZero: true), + weekDays: _weekDays(fields['BYDAY']), + hours: _integers(fields['BYHOUR'], 0, 23), + minutes: _integers(fields['BYMINUTE'], 0, 59), + seconds: _integers(fields['BYSECOND'], 0, 59), + setPositions: _integers(fields['BYSETPOS'], -366, 366, noZero: true), + ); + } + + final int interval; + final int? count; + final DateTime? until; + final bool untilIsUtc; + final List months; + final List monthDays; + final List<_TimeZoneWeekDay> weekDays; + final List hours; + final List minutes; + final List seconds; + final List setPositions; + + List candidates(DateTime prototype, int year) { + final result = []; + final selectedMonths = months.isEmpty ? [prototype.month] : months; + for (final month in selectedMonths) { + final days = _candidateDays(prototype, year, month); + final selectedHours = hours.isEmpty ? [prototype.hour] : hours; + final selectedMinutes = minutes.isEmpty ? [prototype.minute] : minutes; + final selectedSeconds = seconds.isEmpty ? [prototype.second] : seconds; + for (final day in days) { + for (final hour in selectedHours) { + for (final minute in selectedMinutes) { + for (final second in selectedSeconds) { + result.add(DateTime.utc(year, month, day, hour, minute, second)); + } + } + } + } + } + result.sort(); + if (setPositions.isEmpty) return result; + final selected = []; + for (final position in setPositions) { + final index = position > 0 ? position - 1 : result.length + position; + if (index >= 0 && index < result.length) selected.add(result[index]); + } + return selected; + } + + List _candidateDays(DateTime prototype, int year, int month) { + final maximum = _daysInMonth(year, month); + Iterable values; + if (monthDays.isNotEmpty) { + values = monthDays.map((day) => day > 0 ? day : maximum + day + 1); + } else if (weekDays.isNotEmpty) { + final days = []; + for (final weekDay in weekDays) { + if (weekDay.ordinal == null) { + for (var day = 1; day <= maximum; day += 1) { + if (DateTime.utc(year, month, day).weekday == weekDay.weekday) { + days.add(day); + } + } + } else { + final day = _ordinalWeekDay( + year, + month, + weekDay.weekday, + weekDay.ordinal!, + ); + if (day != null) days.add(day); + } + } + values = days; + } else { + values = [prototype.day]; + } + final filtered = + values + .where((day) => day >= 1 && day <= maximum) + .where( + (day) => + weekDays.isEmpty || + weekDays.any( + (rule) => + DateTime.utc(year, month, day).weekday == rule.weekday, + ), + ) + .toSet() + .toList() + ..sort(); + return filtered; + } + + bool isAfterUntil(DateTime local, Duration offsetFrom) { + final limit = until; + if (limit == null) return false; + if (!untilIsUtc) return local.isAfter(limit); + final instant = local.subtract(offsetFrom); + return instant.isAfter(limit); + } +} + +final class _TimeZoneWeekDay { + const _TimeZoneWeekDay(this.ordinal, this.weekday); + + final int? ordinal; + final int weekday; +} + +Duration _parseUtcOffset(String source) { + final match = RegExp(r'^([+-])(\d{2})(\d{2})(\d{2})?$').firstMatch(source); + if (match == null) throw _invalidTimeZone(); + final hours = int.parse(match.group(2)!); + final minutes = int.parse(match.group(3)!); + final seconds = int.tryParse(match.group(4) ?? '') ?? 0; + if (hours > 23 || minutes > 59 || seconds > 59) { + throw _invalidTimeZone(); + } + final total = Duration(hours: hours, minutes: minutes, seconds: seconds); + return match.group(1) == '-' ? -total : total; +} + +List _integers( + String? source, + int minimum, + int maximum, { + bool noZero = false, +}) { + if (source == null) return const []; + final result = []; + for (final token in source.split(',')) { + final value = int.tryParse(token); + if (value == null || + value < minimum || + value > maximum || + (noZero && value == 0)) { + throw _invalidTimeZone(); + } + result.add(value); + if (result.length > 366) throw _unsupportedTimeZoneRule(); + } + return List.unmodifiable(result); +} + +int? _positive(String? source) { + if (source == null) return null; + final value = int.tryParse(source); + if (value == null || value <= 0) throw _invalidTimeZone(); + return value; +} + +List<_TimeZoneWeekDay> _weekDays(String? source) { + if (source == null) return const []; + final result = <_TimeZoneWeekDay>[]; + const weekdays = { + 'MO': DateTime.monday, + 'TU': DateTime.tuesday, + 'WE': DateTime.wednesday, + 'TH': DateTime.thursday, + 'FR': DateTime.friday, + 'SA': DateTime.saturday, + 'SU': DateTime.sunday, + }; + for (final token in source.split(',')) { + final match = RegExp(r'^([+-]?\d{1,2})?([A-Z]{2})$').firstMatch(token); + final weekday = match == null ? null : weekdays[match.group(2)]; + final ordinal = int.tryParse(match?.group(1) ?? ''); + if (weekday == null || ordinal == 0 || (ordinal?.abs() ?? 0) > 53) { + throw _invalidTimeZone(); + } + result.add(_TimeZoneWeekDay(ordinal, weekday)); + } + return List.unmodifiable(result); +} + +int? _ordinalWeekDay(int year, int month, int weekday, int ordinal) { + final maximum = _daysInMonth(year, month); + if (ordinal > 0) { + final firstWeekday = DateTime.utc(year, month).weekday; + final first = 1 + (weekday - firstWeekday) % 7; + final day = first + (ordinal - 1) * 7; + return day <= maximum ? day : null; + } + final lastWeekday = DateTime.utc(year, month, maximum).weekday; + final last = maximum - (lastWeekday - weekday) % 7; + final day = last + (ordinal + 1) * 7; + return day >= 1 ? day : null; +} + +int _daysInMonth(int year, int month) => + DateTime.utc(year, month + 1).subtract(const Duration(days: 1)).day; + +DateTime _parseRuleDateTime(String source) { + final match = RegExp( + r'^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$', + ).firstMatch(source); + if (match == null) throw _invalidTimeZone(); + final parts = [ + for (var index = 1; index <= 6; index += 1) int.parse(match.group(index)!), + ]; + final value = DateTime.utc( + parts[0], + parts[1], + parts[2], + parts[3], + parts[4], + parts[5], + ); + if (value.year != parts[0] || + value.month != parts[1] || + value.day != parts[2] || + value.hour != parts[3] || + value.minute != parts[4] || + value.second != parts[5]) { + throw _invalidTimeZone(); + } + return value; +} + +DavException _invalidTimeZone([ + String code = 'IcalInvalidTimeZoneDefinition', +]) => DavException( + kind: DavErrorKind.invalidCalendarData, + code: code, + safeMessage: 'An embedded iCalendar time zone was invalid.', +); + +DavException _unsupportedTimeZoneRule() => const DavException( + kind: DavErrorKind.unsupportedComponent, + code: 'IcalUnsupportedTimeZoneRule', + safeMessage: 'An embedded iCalendar time zone used an unsupported rule.', +); diff --git a/lib/src/dav/mutation/dav_conditional_mutation_service.dart b/lib/src/dav/mutation/dav_conditional_mutation_service.dart new file mode 100644 index 0000000..9a1dc9e --- /dev/null +++ b/lib/src/dav/mutation/dav_conditional_mutation_service.dart @@ -0,0 +1,924 @@ +import 'package:uuid/uuid.dart'; + +import '../../providers/provider_capabilities.dart'; +import '../dav_errors.dart'; +import '../http/dav_http_transport.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_semantics.dart'; +import '../sync/dav_collection_remote_client.dart'; +import '../xml/dav_xml.dart'; +import 'dav_conflict_analyzer.dart'; +import 'dav_mutation_patch.dart'; + +enum DavConditionalStatus { success, missing, preconditionFailed } + +final class DavConditionalResponse { + const DavConditionalResponse({ + required this.status, + required this.statusCode, + required this.etag, + }); + + final DavConditionalStatus status; + final int statusCode; + final String? etag; +} + +abstract interface class DavMutationRemoteClient { + Future conditionalPut({ + required Uri uri, + required String rawIcs, + required String correlationId, + String? ifMatch, + bool ifNoneMatch = false, + }); + + Future conditionalDelete({ + required Uri uri, + required String ifMatch, + required String correlationId, + }); + + Future conditionalMove({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + required String correlationId, + }); + + Future fetch({ + required String hrefKey, + required Uri uri, + required String correlationId, + }); +} + +final class DavMutationHttpClient implements DavMutationRemoteClient { + DavMutationHttpClient({ + required DavHttpTransport transport, + required String accountId, + required String collectionId, + required DavBasicCredential credential, + DavXmlParser xmlParser = const DavXmlParser(), + }) : _transport = transport, + _accountId = accountId, + _collectionId = collectionId, + _credential = credential, + _xmlParser = xmlParser; + + final DavHttpTransport _transport; + final String _accountId; + final String _collectionId; + final DavBasicCredential _credential; + final DavXmlParser _xmlParser; + + @override + Future conditionalPut({ + required Uri uri, + required String rawIcs, + required String correlationId, + String? ifMatch, + bool ifNoneMatch = false, + }) async { + if ((ifMatch == null) == !ifNoneMatch || ifMatch?.trim().isEmpty == true) { + throw ArgumentError( + 'A conditional PUT requires exactly one precondition.', + ); + } + final response = await _transport.send( + DavRequest.icalendar( + method: 'PUT', + uri: uri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + body: rawIcs, + headers: { + if (ifNoneMatch) 'if-none-match': '*', + if (ifMatch != null) 'if-match': ifMatch, + }, + ), + credential: _credential, + ); + return _conditionalResponse(response, operation: 'update the object'); + } + + @override + Future conditionalDelete({ + required Uri uri, + required String ifMatch, + required String correlationId, + }) async { + if (ifMatch.trim().isEmpty) { + throw ArgumentError('A conditional DELETE requires an exact ETag.'); + } + final response = await _transport.send( + DavRequest( + method: 'DELETE', + uri: uri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: {'if-match': ifMatch}, + retryClass: DavRetryClass.conditionalMutation, + ), + credential: _credential, + ); + return _conditionalResponse(response, operation: 'delete the object'); + } + + @override + Future conditionalMove({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + required String correlationId, + }) async { + if (ifMatch.trim().isEmpty || + sourceUri.scheme != destinationUri.scheme || + sourceUri.host != destinationUri.host || + sourceUri.port != destinationUri.port || + sourceUri.userInfo.isNotEmpty || + destinationUri.userInfo.isNotEmpty || + sourceUri.hasQuery || + destinationUri.hasQuery || + sourceUri.hasFragment || + destinationUri.hasFragment) { + throw ArgumentError('A DAV MOVE requires valid same-origin resources.'); + } + final response = await _transport.send( + DavRequest( + method: 'MOVE', + uri: sourceUri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: { + 'destination': destinationUri.toString(), + 'depth': 'infinity', + 'overwrite': 'F', + 'if-match': ifMatch, + }, + retryClass: DavRetryClass.conditionalMutation, + ), + credential: _credential, + ); + return _conditionalResponse(response, operation: 'move the object'); + } + + @override + Future fetch({ + required String hrefKey, + required Uri uri, + required String correlationId, + }) async { + final response = await _transport.send( + DavRequest( + method: 'GET', + uri: uri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: const {'accept': 'text/calendar'}, + retryClass: DavRetryClass.safeRead, + ), + credential: _credential, + ); + if (response.statusCode == 404) { + return DavFetchedMember.missing(hrefKey: hrefKey, requestUri: uri); + } + if (response.statusCode < 200 || response.statusCode >= 300) { + throw _mutationException(response, operation: 'fetch the object'); + } + final etag = response.etag; + if (etag == null || etag.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavMutationFetchMissingEtag', + safeMessage: 'The DAV object response omitted its ETag.', + correlationId: correlationId, + ); + } + return DavFetchedMember.live( + hrefKey: hrefKey, + requestUri: response.requestUri, + etag: etag, + contentType: response.headers['content-type'], + rawIcsBody: response.bodyText, + ); + } + + DavConditionalResponse _conditionalResponse( + DavResponse response, { + required String operation, + }) { + if (response.statusCode >= 200 && response.statusCode < 300) { + return DavConditionalResponse( + status: DavConditionalStatus.success, + statusCode: response.statusCode, + etag: response.etag, + ); + } + if (response.statusCode == 404) { + return DavConditionalResponse( + status: DavConditionalStatus.missing, + statusCode: response.statusCode, + etag: response.etag, + ); + } + if (response.statusCode == 412) { + return DavConditionalResponse( + status: DavConditionalStatus.preconditionFailed, + statusCode: response.statusCode, + etag: response.etag, + ); + } + throw _mutationException(response, operation: operation); + } + + DavException _mutationException( + DavResponse response, { + required String operation, + }) { + Set conditions = const {}; + if (response.bodyBytes.isNotEmpty) { + try { + conditions = _xmlParser.parseDavError( + response.bodyBytes, + correlationId: response.correlationId, + ); + } on DavException { + // The normal error remains typed by status; raw XML is never exposed. + } + } + bool has(String namespace, String local) => + conditions.contains(DavPropertyName(namespace, local)); + final mapped = switch (response.statusCode) { + 401 => (DavErrorKind.authentication, 'DavAuthRejected'), + 403 => (DavErrorKind.authorization, 'DavPermissionDenied'), + 409 when has(caldavNamespace, 'no-uid-conflict') => ( + DavErrorKind.uidConflict, + 'DavUidConflict', + ), + 409 => (DavErrorKind.conflict, 'DavResourceConflict'), + 423 => (DavErrorKind.conflict, 'DavResourceLocked'), + 429 => (DavErrorKind.rateLimited, 'DavRateLimited'), + 415 => (DavErrorKind.invalidCalendarData, 'DavMalformedResource'), + 507 when has(caldavNamespace, 'max-resource-size') => ( + DavErrorKind.maximumResourceSize, + 'DavMaximumResourceSize', + ), + 507 => (DavErrorKind.limitExceeded, 'DavQuotaOrSizeLimit'), + >= 500 => (DavErrorKind.server, 'DavServerUnavailable'), + _ when has(caldavNamespace, 'valid-calendar-data') => ( + DavErrorKind.invalidCalendarData, + 'DavMalformedResource', + ), + _ when has(caldavNamespace, 'supported-calendar-component') => ( + DavErrorKind.unsupportedComponent, + 'DavUnsupportedComponent', + ), + _ => (DavErrorKind.protocol, 'DavMutationRejected'), + }; + return DavException( + kind: mapped.$1, + code: mapped.$2, + safeMessage: 'The DAV server could not $operation.', + statusCode: response.statusCode, + correlationId: response.correlationId, + retryAfter: parseDavRetryAfter(response.headers['retry-after']), + ); + } +} + +enum DavMutationOutcome { succeeded, conflict } + +final class DavMutationResult { + const DavMutationResult._({ + required this.outcome, + required this.canonicalObject, + required this.conflict, + required this.conflictRemoteObject, + required this.localCandidateRawIcs, + }); + + const DavMutationResult.succeeded(DavFetchedMember? canonicalObject) + : this._( + outcome: DavMutationOutcome.succeeded, + canonicalObject: canonicalObject, + conflict: null, + conflictRemoteObject: null, + localCandidateRawIcs: null, + ); + + const DavMutationResult.conflict( + DavConflictAnalysis conflict, { + DavFetchedMember? remoteObject, + String? localCandidateRawIcs, + }) : this._( + outcome: DavMutationOutcome.conflict, + canonicalObject: null, + conflict: conflict, + conflictRemoteObject: remoteObject, + localCandidateRawIcs: localCandidateRawIcs, + ); + + final DavMutationOutcome outcome; + final DavFetchedMember? canonicalObject; + final DavConflictAnalysis? conflict; + final DavFetchedMember? conflictRemoteObject; + final String? localCandidateRawIcs; +} + +final class DavNewObject { + const DavNewObject({ + required this.uid, + required this.initialMemberName, + required this.rawIcs, + required this.componentType, + }); + + final String uid; + final String initialMemberName; + final String rawIcs; + final String componentType; +} + +final class DavNewObjectFactory { + DavNewObjectFactory({ + String Function()? idFactory, + DateTime Function()? nowUtc, + }) : _idFactory = idFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final String Function() _idFactory; + final DateTime Function() _nowUtc; + + DavNewObject task({ + required String summary, + String? description, + String? dueRaw, + List dueParameters = const [], + }) { + final uid = '${_idFactory()}@busymax.local'; + final timestamp = _utcIcal(_nowUtc()); + final component = IcalComponent( + name: 'VTODO', + children: [ + _newProperty('UID', uid), + _newProperty('DTSTAMP', timestamp), + _newProperty('CREATED', timestamp), + _newProperty('LAST-MODIFIED', timestamp), + _newProperty('SUMMARY', encodeIcalText(summary)), + if (description != null) + _newProperty('DESCRIPTION', encodeIcalText(description)), + if (dueRaw != null) + _newProperty('DUE', dueRaw, parameters: dueParameters), + ], + originalBeginLine: 'BEGIN:VTODO', + originalEndLine: 'END:VTODO', + structurallyDirty: true, + ); + return DavNewObject( + uid: uid, + initialMemberName: '${_idFactory()}.ics', + rawIcs: IcalDocument.create(components: [component]).serialize(), + componentType: 'VTODO', + ); + } + + DavNewObject event({ + required String summary, + required String startRaw, + required List startParameters, + String? endRaw, + List endParameters = const [], + String? durationRaw, + String? description, + String? location, + }) { + if ((endRaw == null) == (durationRaw == null)) { + throw ArgumentError('An event requires exactly one of end or duration.'); + } + final uid = '${_idFactory()}@busymax.local'; + final component = IcalComponent( + name: 'VEVENT', + children: [ + _newProperty('UID', uid), + _newProperty('DTSTAMP', _utcIcal(_nowUtc())), + _newProperty('DTSTART', startRaw, parameters: startParameters), + if (endRaw != null) + _newProperty('DTEND', endRaw, parameters: endParameters), + if (durationRaw != null) _newProperty('DURATION', durationRaw), + _newProperty('SUMMARY', encodeIcalText(summary)), + if (description != null) + _newProperty('DESCRIPTION', encodeIcalText(description)), + if (location != null) + _newProperty('LOCATION', encodeIcalText(location)), + ], + originalBeginLine: 'BEGIN:VEVENT', + originalEndLine: 'END:VEVENT', + structurallyDirty: true, + ); + return DavNewObject( + uid: uid, + initialMemberName: '${_idFactory()}.ics', + rawIcs: IcalDocument.create(components: [component]).serialize(), + componentType: 'VEVENT', + ); + } +} + +final class DavConditionalMutationService { + DavConditionalMutationService({ + required DavMutationRemoteClient remoteClient, + DavConflictAnalyzer conflictAnalyzer = const DavConflictAnalyzer(), + String Function()? memberIdFactory, + DateTime Function()? nowUtc, + this.maximumConditionalAttempts = 3, + }) : _remoteClient = remoteClient, + _conflictAnalyzer = conflictAnalyzer, + _memberIdFactory = memberIdFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final DavMutationRemoteClient _remoteClient; + final DavConflictAnalyzer _conflictAnalyzer; + final String Function() _memberIdFactory; + final DateTime Function() _nowUtc; + final int maximumConditionalAttempts; + + Future create({ + required Uri collectionUri, + required DavNewObject object, + required CollectionCapabilities capabilities, + required String correlationId, + }) async { + final allowed = object.componentType == 'VEVENT' + ? capabilities.canCreateEvent + : object.componentType == 'VTODO' + ? capabilities.canCreateTask + : false; + if (!allowed) throw _readOnlyError(correlationId); + var memberName = object.initialMemberName; + for (var attempt = 0; attempt < maximumConditionalAttempts; attempt += 1) { + final uri = _memberUri(collectionUri, memberName); + final hrefKey = uri.path; + try { + final response = await _remoteClient.conditionalPut( + uri: uri, + rawIcs: object.rawIcs, + correlationId: correlationId, + ifNoneMatch: true, + ); + if (response.status == DavConditionalStatus.preconditionFailed) { + memberName = '${_memberIdFactory()}.ics'; + continue; + } + if (response.status == DavConditionalStatus.missing) continue; + final canonical = await _remoteClient.fetch( + hrefKey: hrefKey, + uri: uri, + correlationId: correlationId, + ); + if (!canonical.missing && + _sameIntendedObject(object.rawIcs, canonical.rawIcsBody!)) { + return DavMutationResult.succeeded(canonical); + } + } on DavException catch (error) { + if (!_isUnknownOutcome(error)) rethrow; + final resolved = await _remoteClient.fetch( + hrefKey: hrefKey, + uri: uri, + correlationId: correlationId, + ); + if (!resolved.missing) { + if (_sameIntendedObject(object.rawIcs, resolved.rawIcsBody!)) { + return DavMutationResult.succeeded(resolved); + } + memberName = '${_memberIdFactory()}.ics'; + } + } + } + throw DavException( + kind: DavErrorKind.conflict, + code: 'DavCreateCollisionLimitExceeded', + safeMessage: 'The DAV server could not allocate a new object name.', + correlationId: correlationId, + ); + } + + Future update({ + required String hrefKey, + required Uri uri, + required String baselineEtag, + required String baselineRawIcs, + required DavMutationPatch patch, + required CollectionCapabilities capabilities, + required String correlationId, + }) async { + final event = patch.target.componentType.toUpperCase() == 'VEVENT'; + if (baselineEtag.isEmpty || + (event ? !capabilities.canUpdateEvent : !capabilities.canUpdateTask)) { + throw _readOnlyError(correlationId); + } + var expectedEtag = baselineEtag; + var comparisonBaseline = baselineRawIcs; + var candidate = patch.applyTo(baselineRawIcs, nowUtc: _nowUtc()); + DavFetchedMember? lastCurrent; + for (var attempt = 0; attempt < maximumConditionalAttempts; attempt += 1) { + try { + final response = await _remoteClient.conditionalPut( + uri: uri, + rawIcs: candidate, + correlationId: correlationId, + ifMatch: expectedEtag, + ); + if (response.status == DavConditionalStatus.success) { + final canonical = await _remoteClient.fetch( + hrefKey: hrefKey, + uri: uri, + correlationId: correlationId, + ); + if (!canonical.missing) { + return DavMutationResult.succeeded(canonical); + } + } + } on DavException catch (error) { + if (!_isUnknownOutcome(error)) rethrow; + } + + final current = await _remoteClient.fetch( + hrefKey: hrefKey, + uri: uri, + correlationId: correlationId, + ); + lastCurrent = current; + if (current.missing) { + return DavMutationResult.conflict( + _resourceMissingConflict(patch.changedProperties), + remoteObject: current, + localCandidateRawIcs: candidate, + ); + } + if (_sameIntendedObject(candidate, current.rawIcsBody!)) { + return DavMutationResult.succeeded(current); + } + final analysis = _conflictAnalyzer.analyzeUpdate( + baselineRawIcs: comparisonBaseline, + currentRemoteRawIcs: current.rawIcsBody!, + localPatch: patch, + nowUtc: _nowUtc(), + ); + if (!analysis.canRetryWithRemoteEtag) { + return DavMutationResult.conflict( + analysis, + remoteObject: current, + localCandidateRawIcs: candidate, + ); + } + expectedEtag = current.etag!; + comparisonBaseline = current.rawIcsBody!; + candidate = analysis.mergedRawIcs!; + } + return DavMutationResult.conflict( + _retryLimitConflict(patch.changedProperties), + remoteObject: lastCurrent, + localCandidateRawIcs: candidate, + ); + } + + Future move({ + required String sourceHrefKey, + required Uri sourceUri, + required String destinationHrefKey, + required Uri destinationUri, + required String baselineEtag, + required String baselineRawIcs, + required bool isEvent, + required CollectionCapabilities sourceCapabilities, + required CollectionCapabilities destinationCapabilities, + required String correlationId, + DavMutationPatch? postMovePatch, + }) async { + final canDelete = isEvent + ? sourceCapabilities.canDeleteEvent + : sourceCapabilities.canDeleteTask; + final canCreate = isEvent + ? destinationCapabilities.canCreateEvent + : destinationCapabilities.canCreateTask; + final canUpdate = isEvent + ? destinationCapabilities.canUpdateEvent + : destinationCapabilities.canUpdateTask; + if (baselineEtag.isEmpty || + !canDelete || + !canCreate || + (postMovePatch != null && !canUpdate) || + sourceHrefKey == destinationHrefKey || + sourceUri.scheme != destinationUri.scheme || + sourceUri.host != destinationUri.host || + sourceUri.port != destinationUri.port) { + throw _readOnlyError(correlationId); + } + final sourceSemantic = IcalSemanticDocument.parse(baselineRawIcs); + if (sourceSemantic.components.isEmpty || + sourceSemantic.components.first.componentType != + (isEvent ? 'VEVENT' : 'VTODO') || + (postMovePatch != null && + (postMovePatch.target.componentType != + sourceSemantic.components.first.componentType || + postMovePatch.target.uid != sourceSemantic.primaryUid))) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavMoveObjectInvalid', + safeMessage: 'The DAV object could not be moved safely.', + ); + } + + var expectedEtag = baselineEtag; + var expectedRawIcs = baselineRawIcs; + DavFetchedMember? lastRemote; + for (var attempt = 0; attempt < maximumConditionalAttempts; attempt += 1) { + try { + final response = await _remoteClient.conditionalMove( + sourceUri: sourceUri, + destinationUri: destinationUri, + ifMatch: expectedEtag, + correlationId: correlationId, + ); + if (response.status == DavConditionalStatus.success) { + final destination = await _remoteClient.fetch( + hrefKey: destinationHrefKey, + uri: destinationUri, + correlationId: correlationId, + ); + if (!destination.missing && + _sameIntendedObject(expectedRawIcs, destination.rawIcsBody!)) { + return _finishMoveAtDestination( + destination, + postMovePatch: postMovePatch, + destinationCapabilities: destinationCapabilities, + correlationId: correlationId, + ); + } + } + } on DavException catch (error) { + if (!_isUnknownOutcome(error)) rethrow; + } + + final destination = await _remoteClient.fetch( + hrefKey: destinationHrefKey, + uri: destinationUri, + correlationId: correlationId, + ); + final source = await _remoteClient.fetch( + hrefKey: sourceHrefKey, + uri: sourceUri, + correlationId: correlationId, + ); + lastRemote = destination.missing ? source : destination; + if (source.missing) { + if (destination.missing) { + return DavMutationResult.conflict( + _moveConflict('DavConflictMoveSourceRemoved', const {'DELETE'}), + remoteObject: source, + localCandidateRawIcs: _moveCandidate( + expectedRawIcs, + postMovePatch, + _nowUtc(), + ), + ); + } + final destinationRaw = destination.rawIcsBody!; + final alreadyFinal = _sameIntendedObject( + _moveCandidate(expectedRawIcs, postMovePatch, _nowUtc()), + destinationRaw, + ); + if (alreadyFinal) { + return DavMutationResult.succeeded(destination); + } + if (_sameIntendedObject(expectedRawIcs, destinationRaw)) { + return _finishMoveAtDestination( + destination, + postMovePatch: postMovePatch, + destinationCapabilities: destinationCapabilities, + correlationId: correlationId, + ); + } + return DavMutationResult.conflict( + _moveConflict('DavConflictMoveDestinationChanged', const { + 'RESOURCE', + }), + remoteObject: destination, + localCandidateRawIcs: _moveCandidate( + expectedRawIcs, + postMovePatch, + _nowUtc(), + ), + ); + } + if (!destination.missing) { + return DavMutationResult.conflict( + _moveConflict('DavConflictMoveDestinationExists', const { + 'DESTINATION', + }), + remoteObject: destination, + localCandidateRawIcs: _moveCandidate( + expectedRawIcs, + postMovePatch, + _nowUtc(), + ), + ); + } + if (!_sameIntendedObject(expectedRawIcs, source.rawIcsBody!)) { + return DavMutationResult.conflict( + _moveConflict('DavConflictStaleMove', const {'RESOURCE'}), + remoteObject: source, + localCandidateRawIcs: _moveCandidate( + expectedRawIcs, + postMovePatch, + _nowUtc(), + ), + ); + } + expectedEtag = source.etag!; + expectedRawIcs = source.rawIcsBody!; + } + return DavMutationResult.conflict( + _retryLimitConflict(const {'MOVE'}), + remoteObject: lastRemote, + localCandidateRawIcs: _moveCandidate( + expectedRawIcs, + postMovePatch, + _nowUtc(), + ), + ); + } + + Future _finishMoveAtDestination( + DavFetchedMember destination, { + required DavMutationPatch? postMovePatch, + required CollectionCapabilities destinationCapabilities, + required String correlationId, + }) { + if (postMovePatch == null) { + return Future.value(DavMutationResult.succeeded(destination)); + } + return update( + hrefKey: destination.hrefKey, + uri: destination.requestUri, + baselineEtag: destination.etag!, + baselineRawIcs: destination.rawIcsBody!, + patch: postMovePatch, + capabilities: destinationCapabilities, + correlationId: correlationId, + ); + } + + Future delete({ + required String hrefKey, + required Uri uri, + required String baselineEtag, + required String baselineRawIcs, + required bool isEvent, + required CollectionCapabilities capabilities, + required String correlationId, + }) async { + if (baselineEtag.isEmpty || + (isEvent + ? !capabilities.canDeleteEvent + : !capabilities.canDeleteTask)) { + throw _readOnlyError(correlationId); + } + var expectedEtag = baselineEtag; + var comparisonBaseline = baselineRawIcs; + DavFetchedMember? lastCurrent; + for (var attempt = 0; attempt < maximumConditionalAttempts; attempt += 1) { + try { + final response = await _remoteClient.conditionalDelete( + uri: uri, + ifMatch: expectedEtag, + correlationId: correlationId, + ); + if (response.status == DavConditionalStatus.success || + response.status == DavConditionalStatus.missing) { + return const DavMutationResult.succeeded(null); + } + } on DavException catch (error) { + if (!_isUnknownOutcome(error)) rethrow; + } + final current = await _remoteClient.fetch( + hrefKey: hrefKey, + uri: uri, + correlationId: correlationId, + ); + lastCurrent = current; + if (current.missing) { + return const DavMutationResult.succeeded(null); + } + final analysis = _conflictAnalyzer.analyzeDelete( + baselineRawIcs: comparisonBaseline, + currentRemoteRawIcs: current.rawIcsBody!, + ); + if (!analysis.canRetryWithRemoteEtag) { + return DavMutationResult.conflict( + analysis, + remoteObject: current, + localCandidateRawIcs: baselineRawIcs, + ); + } + expectedEtag = current.etag!; + comparisonBaseline = current.rawIcsBody!; + } + return DavMutationResult.conflict( + _retryLimitConflict(const {'DELETE'}), + remoteObject: lastCurrent, + localCandidateRawIcs: baselineRawIcs, + ); + } +} + +IcalProperty _newProperty( + String name, + String value, { + List parameters = const [], +}) => IcalProperty( + group: null, + name: name, + parameters: List.unmodifiable(parameters), + rawValue: value, + originalPhysicalLines: const [], + isDirty: true, +); + +Uri _memberUri(Uri collectionUri, String memberName) { + if (!RegExp(r'^[A-Za-z0-9-]+[.]ics$').hasMatch(memberName)) { + throw ArgumentError.value(memberName, 'memberName'); + } + final base = collectionUri.path.endsWith('/') + ? collectionUri + : collectionUri.replace(path: '${collectionUri.path}/'); + return base.resolve(memberName); +} + +bool _sameIntendedObject(String intended, String current) { + try { + final intendedSemantic = IcalSemanticDocument.parse(intended); + final currentSemantic = IcalSemanticDocument.parse(current); + return intendedSemantic.primaryUid == currentSemantic.primaryUid && + intendedSemantic.semanticHash == currentSemantic.semanticHash; + } on DavException { + return false; + } +} + +bool _isUnknownOutcome(DavException error) => + error.kind == DavErrorKind.network || error.kind == DavErrorKind.timeout; + +DavException _readOnlyError(String correlationId) => DavException( + kind: DavErrorKind.authorization, + code: 'DavReadOnly', + safeMessage: 'This DAV collection does not allow that change.', + correlationId: correlationId, +); + +DavConflictAnalysis _resourceMissingConflict(Set localChanges) => + DavConflictAnalysis( + outcome: DavConflictOutcome.conflict, + localChangedProperties: Set.unmodifiable(localChanges), + remoteChangedProperties: const {'DELETE'}, + mergedRawIcs: null, + conflictCode: 'DavConflictRemoteObjectRemoved', + ); + +DavConflictAnalysis _retryLimitConflict(Set localChanges) => + DavConflictAnalysis( + outcome: DavConflictOutcome.conflict, + localChangedProperties: Set.unmodifiable(localChanges), + remoteChangedProperties: const {'RESOURCE'}, + mergedRawIcs: null, + conflictCode: 'DavConflictRetryLimitExceeded', + ); + +DavConflictAnalysis _moveConflict(String code, Set remoteChanges) => + DavConflictAnalysis( + outcome: DavConflictOutcome.conflict, + localChangedProperties: const {'MOVE'}, + remoteChangedProperties: Set.unmodifiable(remoteChanges), + mergedRawIcs: null, + conflictCode: code, + ); + +String _moveCandidate( + String sourceRawIcs, + DavMutationPatch? postMovePatch, + DateTime nowUtc, +) => postMovePatch == null + ? sourceRawIcs + : postMovePatch.applyTo(sourceRawIcs, nowUtc: nowUtc.toUtc()); + +String _utcIcal(DateTime value) { + final utc = value.toUtc(); + String two(int number) => number.toString().padLeft(2, '0'); + return '${utc.year.toString().padLeft(4, '0')}${two(utc.month)}' + '${two(utc.day)}T${two(utc.hour)}${two(utc.minute)}${two(utc.second)}Z'; +} diff --git a/lib/src/dav/mutation/dav_conflict_analyzer.dart b/lib/src/dav/mutation/dav_conflict_analyzer.dart new file mode 100644 index 0000000..6622af2 --- /dev/null +++ b/lib/src/dav/mutation/dav_conflict_analyzer.dart @@ -0,0 +1,182 @@ +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_semantics.dart'; +import 'dav_mutation_patch.dart'; + +enum DavConflictOutcome { remoteUnchanged, autoMerged, conflict } + +final class DavConflictAnalysis { + const DavConflictAnalysis({ + required this.outcome, + required this.localChangedProperties, + required this.remoteChangedProperties, + required this.mergedRawIcs, + required this.conflictCode, + }); + + final DavConflictOutcome outcome; + final Set localChangedProperties; + final Set remoteChangedProperties; + final String? mergedRawIcs; + final String? conflictCode; + + bool get canRetryWithRemoteEtag => + outcome == DavConflictOutcome.remoteUnchanged || + outcome == DavConflictOutcome.autoMerged; +} + +final class DavConflictAnalyzer { + const DavConflictAnalyzer(); + + DavConflictAnalysis analyzeUpdate({ + required String baselineRawIcs, + required String currentRemoteRawIcs, + required DavMutationPatch localPatch, + required DateTime nowUtc, + }) { + final baseline = IcalSemanticDocument.parse(baselineRawIcs); + final current = IcalSemanticDocument.parse(currentRemoteRawIcs); + final localChanges = localPatch.changedProperties; + if (baseline.semanticHash == current.semanticHash) { + return DavConflictAnalysis( + outcome: DavConflictOutcome.remoteUnchanged, + localChangedProperties: Set.unmodifiable(localChanges), + remoteChangedProperties: const {}, + mergedRawIcs: localPatch.applyTo(currentRemoteRawIcs, nowUtc: nowUtc), + conflictCode: null, + ); + } + + final baselineKeys = _componentKeys(baseline); + final currentKeys = _componentKeys(current); + if (!_sameSet(baselineKeys, currentKeys)) { + return _conflict(localChanges, const { + 'RECURRENCE-SET', + }, 'DavConflictRecurrenceSetChanged'); + } + // Adding or deleting a component changes the resource/recurrence set as a + // whole. It is safe only when the remote resource is byte-semantically at + // the baseline (handled above); any concurrent semantic edit is manual. + if (localChanges.contains('COMPONENT-SET')) { + return _conflict(localChanges, const { + 'RESOURCE', + }, 'DavConflictBroadRecurrenceChange'); + } + final baselineTarget = _requireTarget(baseline, localPatch.target); + final currentTarget = _requireTarget(current, localPatch.target); + final remoteChanges = changedIcalProperties( + baselineTarget.documentComponent, + currentTarget.documentComponent, + ); + if (_nestedComponentHash(baselineTarget.documentComponent) != + _nestedComponentHash(currentTarget.documentComponent)) { + remoteChanges.add('VALARM'); + } + if (_timeZoneHash(baseline) != _timeZoneHash(current)) { + remoteChanges.add('VTIMEZONE'); + } + final broadRemote = remoteChanges.any( + const { + 'UID', + 'DTSTART', + 'RRULE', + 'RDATE', + 'EXDATE', + 'RECURRENCE-ID', + 'RECURRENCE-SET', + 'VTIMEZONE', + }.contains, + ); + final overlap = localChanges.intersection(remoteChanges); + if (overlap.isNotEmpty || + (localPatch.isBroadRecurrenceMutation && remoteChanges.isNotEmpty) || + (broadRemote && localChanges.isNotEmpty)) { + return _conflict( + localChanges, + remoteChanges, + overlap.isNotEmpty + ? 'DavConflictOverlappingProperties' + : 'DavConflictBroadRecurrenceChange', + ); + } + final merged = localPatch.applyTo(currentRemoteRawIcs, nowUtc: nowUtc); + return DavConflictAnalysis( + outcome: DavConflictOutcome.autoMerged, + localChangedProperties: Set.unmodifiable(localChanges), + remoteChangedProperties: Set.unmodifiable(remoteChanges), + mergedRawIcs: merged, + conflictCode: null, + ); + } + + DavConflictAnalysis analyzeDelete({ + required String baselineRawIcs, + required String currentRemoteRawIcs, + }) { + final baseline = IcalSemanticDocument.parse(baselineRawIcs); + final current = IcalSemanticDocument.parse(currentRemoteRawIcs); + if (baseline.semanticHash == current.semanticHash) { + return const DavConflictAnalysis( + outcome: DavConflictOutcome.remoteUnchanged, + localChangedProperties: {'DELETE'}, + remoteChangedProperties: {}, + mergedRawIcs: null, + conflictCode: null, + ); + } + return const DavConflictAnalysis( + outcome: DavConflictOutcome.conflict, + localChangedProperties: {'DELETE'}, + remoteChangedProperties: {'RESOURCE'}, + mergedRawIcs: null, + conflictCode: 'DavConflictStaleDelete', + ); + } +} + +DavConflictAnalysis _conflict( + Set local, + Set remote, + String code, +) => DavConflictAnalysis( + outcome: DavConflictOutcome.conflict, + localChangedProperties: Set.unmodifiable(local), + remoteChangedProperties: Set.unmodifiable(remote), + mergedRawIcs: null, + conflictCode: code, +); + +IcalSemanticComponent _requireTarget( + IcalSemanticDocument document, + IcalComponentKey target, +) { + final matches = document.components.where( + (component) => + component.componentType == target.componentType.toUpperCase() && + component.uid == target.uid && + component.recurrenceIdKey == target.recurrenceIdKey, + ); + if (matches.length != 1) { + throw const DavException( + kind: DavErrorKind.conflict, + code: 'DavConflictTargetComponentChanged', + safeMessage: 'The edited calendar component changed on the server.', + ); + } + return matches.single; +} + +Set _componentKeys(IcalSemanticDocument document) => { + for (final component in document.components) + '${component.componentType}\u0000${component.uid}\u0000' + '${component.recurrenceIdKey ?? ''}', +}; + +bool _sameSet(Set left, Set right) => + left.length == right.length && left.containsAll(right); + +String _nestedComponentHash(IcalComponent component) => + component.components.map(semanticComponentHash).join('\n'); + +String _timeZoneHash(IcalSemanticDocument document) => + document.timeZones.map(semanticComponentHash).join('\n'); diff --git a/lib/src/dav/mutation/dav_conflict_repository.dart b/lib/src/dav/mutation/dav_conflict_repository.dart new file mode 100644 index 0000000..a129d55 --- /dev/null +++ b/lib/src/dav/mutation/dav_conflict_repository.dart @@ -0,0 +1,440 @@ +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../db/app_database.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_semantics.dart'; +import '../storage/dav_object_repository.dart'; +import 'dav_conditional_mutation_service.dart'; +import 'dav_mutation_patch.dart'; +import 'dav_pending_operations.dart'; + +enum DavConflictResolution { keepServer, reapplyLocal, duplicateLocal } + +final class DavConflictEntity { + const DavConflictEntity({ + required this.id, + required this.accountId, + required this.provider, + required this.accountLabel, + required this.collectionName, + required this.itemTitle, + required this.componentType, + required this.remoteChangedAtUtc, + required this.localEditSummary, + required this.conflictCode, + required this.canKeepServer, + required this.canReapplyLocal, + required this.canDuplicate, + }); + + final String id; + final String accountId; + final BusyProvider provider; + final String accountLabel; + final String collectionName; + final String itemTitle; + final String componentType; + final DateTime? remoteChangedAtUtc; + final String localEditSummary; + final String conflictCode; + final bool canKeepServer; + final bool canReapplyLocal; + final bool canDuplicate; +} + +final class DavConflictRepository { + DavConflictRepository({required AppDatabase database}) : _database = database; + + final AppDatabase _database; + + Stream> watchUnresolved() { + final query = + _database.select(_database.davConflictSnapshots).join([ + innerJoin( + _database.accounts, + _database.accounts.id.equalsExp( + _database.davConflictSnapshots.accountId, + ), + ), + leftOuterJoin( + _database.davCollections, + _database.davCollections.id.equalsExp( + _database.davConflictSnapshots.davCollectionId, + ), + ), + leftOuterJoin( + _database.pendingOps, + _database.pendingOps.conflictSnapshotId.equalsExp( + _database.davConflictSnapshots.id, + ), + ), + ]) + ..where(_database.davConflictSnapshots.resolvedAtUtc.isNull()) + ..orderBy([ + OrderingTerm.desc(_database.davConflictSnapshots.createdAtUtc), + ]); + return query.watch().map((rows) { + return [ + for (final row in rows) + _entity( + row.readTable(_database.davConflictSnapshots), + row.readTable(_database.accounts), + row.readTableOrNull(_database.davCollections), + row.readTableOrNull(_database.pendingOps), + ), + ]; + }); + } +} + +final class DavConflictResolutionService { + DavConflictResolutionService({ + required AppDatabase database, + DavObjectRepository? objectRepository, + DavPendingOperationQueue? pendingQueue, + String Function()? idFactory, + DateTime Function()? nowUtc, + }) : _database = database, + _objectRepository = + objectRepository ?? DavObjectRepository(database: database), + _pendingQueue = + pendingQueue ?? DavPendingOperationQueue(database: database), + _idFactory = idFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final AppDatabase _database; + final DavObjectRepository _objectRepository; + final DavPendingOperationQueue _pendingQueue; + final String Function() _idFactory; + final DateTime Function() _nowUtc; + + Future resolve(String snapshotId, DavConflictResolution resolution) { + return switch (resolution) { + DavConflictResolution.keepServer => _keepServer(snapshotId), + DavConflictResolution.reapplyLocal => _reapplyLocal(snapshotId), + DavConflictResolution.duplicateLocal => _duplicateLocal(snapshotId), + }; + } + + Future _keepServer(String snapshotId) async { + final context = await _context(snapshotId); + await _adoptRemote(context, DavConflictResolution.keepServer); + } + + Future _adoptRemote( + _ResolutionContext context, + DavConflictResolution resolution, + ) async { + final snapshot = context.snapshot; + final object = context.object; + if (object == null || + snapshot.remoteEtag == null || + snapshot.remoteRawIcs.isEmpty) { + throw _resolutionUnavailable(); + } + final provider = BusyProviderCodec.requireStorageValue( + context.account.provider, + ); + await _objectRepository.commitConfirmedMutation( + accountId: snapshot.accountId, + collectionId: context.collection.id, + provider: provider, + canonicalObject: DavPreparedObject.parse( + hrefKey: object.hrefKey, + requestUri: Uri.parse(object.requestUri), + etag: snapshot.remoteEtag, + contentType: object.contentType, + rawIcsBody: snapshot.remoteRawIcs, + maximumResourceBytes: + context.collection.maximumResourceSize ?? 16 * 1024 * 1024, + ), + completedAtUtc: _nowUtc(), + ); + await _finish(context, resolution); + } + + Future _reapplyLocal(String snapshotId) async { + final context = await _context(snapshotId); + final snapshot = context.snapshot; + final operation = context.operation; + if (snapshot.remoteEtag == null || + snapshot.remoteRawIcs.isEmpty || + operation.mutationPatchJson == null) { + throw _resolutionUnavailable(); + } + final patch = DavMutationPatch.fromJsonString(operation.mutationPatchJson!); + patch.applyTo(snapshot.remoteRawIcs, nowUtc: _nowUtc()); + final now = _nowUtc().toIso8601String(); + await _database.transaction(() async { + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(operation.id))).write( + PendingOpsCompanion( + baselineEtag: Value(snapshot.remoteEtag), + baselineRawIcs: Value(snapshot.remoteRawIcs), + state: const Value('pending'), + conflictState: const Value(null), + conflictSnapshotId: const Value(null), + retryClassification: const Value('conditional_update'), + attemptCount: const Value(0), + nextAttemptAtUtc: const Value(null), + lastErrorCode: const Value(null), + lastErrorMessage: const Value(null), + updatedAtUtc: Value(now), + ), + ); + await _markSnapshotResolved( + snapshot.id, + DavConflictResolution.reapplyLocal, + now, + ); + }); + } + + Future _duplicateLocal(String snapshotId) async { + final context = await _context(snapshotId); + final local = context.snapshot.localCandidateRawIcs; + if (local.isEmpty) throw _resolutionUnavailable(); + final duplicated = _duplicateResource( + local, + uid: '${_idFactory()}@busymax.local', + nowUtc: _nowUtc(), + ); + final semantic = IcalSemanticDocument.parse(duplicated); + final componentType = semantic.components.first.componentType; + final uid = semantic.primaryUid!; + await _pendingQueue.enqueueCreate( + accountId: context.snapshot.accountId, + collectionId: context.collection.id, + object: DavNewObject( + uid: uid, + initialMemberName: '${_idFactory()}.ics', + rawIcs: duplicated, + componentType: componentType, + ), + ); + if (context.snapshot.remoteEtag != null && + context.snapshot.remoteRawIcs.isNotEmpty && + context.object != null) { + await _adoptRemote(context, DavConflictResolution.duplicateLocal); + return; + } + await _finish(context, DavConflictResolution.duplicateLocal); + } + + Future<_ResolutionContext> _context(String snapshotId) async { + final snapshot = + await (_database.select(_database.davConflictSnapshots)..where( + (row) => row.id.equals(snapshotId) & row.resolvedAtUtc.isNull(), + )) + .getSingleOrNull(); + if (snapshot == null) throw _resolutionUnavailable(); + final operation = + await (_database.select(_database.pendingOps)..where( + (row) => + row.conflictSnapshotId.equals(snapshotId) & + row.state.equals('conflict'), + )) + .getSingleOrNull(); + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(snapshot.accountId))).getSingleOrNull(); + final collectionId = snapshot.davCollectionId; + final collection = collectionId == null + ? null + : await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingleOrNull(); + final objectId = snapshot.davObjectId; + final object = objectId == null + ? null + : await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (operation == null || account == null || collection == null) { + throw _resolutionUnavailable(); + } + return _ResolutionContext( + snapshot: snapshot, + operation: operation, + account: account, + collection: collection, + object: object, + ); + } + + Future _finish( + _ResolutionContext context, + DavConflictResolution resolution, + ) async { + final now = _nowUtc().toIso8601String(); + await _database.transaction(() async { + await _database.pendingOpsDao.deleteOp(context.operation.id); + await _markSnapshotResolved(context.snapshot.id, resolution, now); + }); + } + + Future _markSnapshotResolved( + String id, + DavConflictResolution resolution, + String now, + ) { + return (_database.update( + _database.davConflictSnapshots, + )..where((row) => row.id.equals(id))).write( + DavConflictSnapshotsCompanion( + resolvedAtUtc: Value(now), + resolution: Value(resolution.name), + ), + ); + } +} + +final class _ResolutionContext { + const _ResolutionContext({ + required this.snapshot, + required this.operation, + required this.account, + required this.collection, + required this.object, + }); + + final DavConflictSnapshot snapshot; + final PendingOp operation; + final Account account; + final DavCollection collection; + final DavObject? object; +} + +DavConflictEntity _entity( + DavConflictSnapshot snapshot, + Account account, + DavCollection? collection, + PendingOp? operation, +) { + final local = _trySemantic(snapshot.localCandidateRawIcs); + final remote = _trySemantic(snapshot.remoteRawIcs); + final component = + local?.components.firstOrNull ?? remote?.components.firstOrNull; + final provider = BusyProviderCodec.requireStorageValue(account.provider); + final patch = operation?.mutationPatchJson == null + ? null + : _tryPatch(operation!.mutationPatchJson!); + return DavConflictEntity( + id: snapshot.id, + accountId: snapshot.accountId, + provider: provider, + accountLabel: _accountLabel(account, provider), + collectionName: collection?.displayName ?? provider.displayName, + itemTitle: component?.summary?.trim().isNotEmpty == true + ? component!.summary!.trim() + : '(untitled)', + componentType: component?.componentType ?? operation?.entityType ?? 'item', + remoteChangedAtUtc: _remoteChangedAt(remote), + localEditSummary: _editSummary(operation, patch), + conflictCode: snapshot.conflictCode, + canKeepServer: + snapshot.remoteEtag != null && snapshot.remoteRawIcs.isNotEmpty, + canReapplyLocal: + operation?.operationType == 'dav.update' && + snapshot.remoteEtag != null && + snapshot.remoteRawIcs.isNotEmpty && + patch != null, + canDuplicate: snapshot.localCandidateRawIcs.isNotEmpty, + ); +} + +IcalSemanticDocument? _trySemantic(String source) { + if (source.isEmpty) return null; + try { + return IcalSemanticDocument.parse(source); + } on Object { + return null; + } +} + +DavMutationPatch? _tryPatch(String source) { + try { + return DavMutationPatch.fromJsonString(source); + } on Object { + return null; + } +} + +DateTime? _remoteChangedAt(IcalSemanticDocument? document) { + if (document == null) return null; + final component = document.components.first; + final temporal = component.lastModified ?? component.dtstamp; + if (temporal == null) return null; + final raw = temporal.rawValue; + if (!raw.endsWith('Z') || raw.length < 16) return null; + return DateTime.tryParse( + '${raw.substring(0, 4)}-${raw.substring(4, 6)}-' + '${raw.substring(6, 8)}T${raw.substring(9, 11)}:' + '${raw.substring(11, 13)}:${raw.substring(13, 15)}Z', + ); +} + +String _editSummary(PendingOp? operation, DavMutationPatch? patch) { + if (operation == null) return 'Pending DAV change'; + if (operation.operationType == 'dav.create') return 'Create item'; + if (operation.operationType == 'dav.delete') return 'Delete item'; + final names = patch?.changedProperties.toList(); + names?.sort(); + return names == null || names.isEmpty + ? 'Update item' + : 'Update ${names.join(', ')}'; +} + +String _accountLabel(Account account, BusyProvider provider) { + final display = account.displayName?.trim(); + if (display != null && display.isNotEmpty) return display; + final email = account.email?.trim(); + if (email != null && email.isNotEmpty) return email; + return provider.displayName; +} + +String _duplicateResource( + String source, { + required String uid, + required DateTime nowUtc, +}) { + final document = IcalDocument.parse(source); + final stamp = _utcIcal(nowUtc); + for (final component in document.calendarComponents.where( + (component) => component.name == 'VEVENT' || component.name == 'VTODO', + )) { + final uidProperty = component.firstProperty('UID'); + if (uidProperty == null) throw _resolutionUnavailable(); + uidProperty + ..rawValue = uid + ..isDirty = true; + final stampProperty = component.firstProperty('DTSTAMP'); + if (stampProperty != null) { + stampProperty + ..rawValue = stamp + ..isDirty = true; + } + component.structurallyDirty = true; + } + document.root.structurallyDirty = true; + final serialized = document.serialize(); + IcalSemanticDocument.parse(serialized); + return serialized; +} + +String _utcIcal(DateTime value) { + final utc = value.toUtc(); + String two(int number) => number.toString().padLeft(2, '0'); + return '${utc.year.toString().padLeft(4, '0')}${two(utc.month)}' + '${two(utc.day)}T${two(utc.hour)}${two(utc.minute)}${two(utc.second)}Z'; +} + +DavException _resolutionUnavailable() => const DavException( + kind: DavErrorKind.conflict, + code: 'DavConflictResolutionUnavailable', + safeMessage: 'That conflict resolution is not available for this item.', +); diff --git a/lib/src/dav/mutation/dav_mutation_patch.dart b/lib/src/dav/mutation/dav_mutation_patch.dart new file mode 100644 index 0000000..57f5468 --- /dev/null +++ b/lib/src/dav/mutation/dav_mutation_patch.dart @@ -0,0 +1,675 @@ +import 'dart:convert'; + +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_semantics.dart'; + +const davMutationPatchSchemaVersion = 1; + +enum DavMutationScope { + object, + recurrenceMaster, + recurrenceException, + occurrence, + collection, +} + +enum DavPatchOperationType { + setText, + setRaw, + replaceRepeatedRaw, + setTaskProgress, + setTaskParent, + replaceAlarm, + addComponent, + removeComponent, +} + +final class DavRawPropertyValue { + const DavRawPropertyValue({required this.value, this.parameters = const []}); + + final String value; + final List parameters; + + Map toJson() => { + 'value': value, + 'parameters': [ + for (final parameter in parameters) + { + 'name': parameter.name, + 'values': parameter.values, + 'wasQuoted': parameter.wasQuoted, + }, + ], + }; + + factory DavRawPropertyValue.fromJson(Map json) => + DavRawPropertyValue( + value: _requiredString(json, 'value'), + parameters: _parameters(json['parameters']), + ); +} + +final class DavPatchOperation { + const DavPatchOperation._({ + required this.type, + required this.propertyName, + this.value, + this.values = const [], + this.parameters = const [], + this.percentComplete, + this.completedAtUtc, + this.alarmIndex, + this.alarm, + this.component, + this.componentKey, + }); + + factory DavPatchOperation.setText(String propertyName, String? value) => + DavPatchOperation._( + type: DavPatchOperationType.setText, + propertyName: _validatedEditableProperty(propertyName), + value: value, + ); + + factory DavPatchOperation.setRaw( + String propertyName, + String? value, { + List parameters = const [], + }) => DavPatchOperation._( + type: DavPatchOperationType.setRaw, + propertyName: _validatedEditableProperty(propertyName), + value: value, + parameters: List.unmodifiable(parameters), + ); + + factory DavPatchOperation.replaceRepeatedRaw( + String propertyName, + List values, + ) => DavPatchOperation._( + type: DavPatchOperationType.replaceRepeatedRaw, + propertyName: _validatedEditableProperty(propertyName), + values: List.unmodifiable(values), + ); + + factory DavPatchOperation.setTaskProgress( + int percentComplete, { + DateTime? completedAtUtc, + }) { + if (percentComplete < 0 || percentComplete > 100) { + throw ArgumentError.value(percentComplete, 'percentComplete'); + } + return DavPatchOperation._( + type: DavPatchOperationType.setTaskProgress, + propertyName: 'TASK-PROGRESS', + percentComplete: percentComplete, + completedAtUtc: completedAtUtc?.toUtc(), + ); + } + + factory DavPatchOperation.setTaskParent(String? parentUid) => + DavPatchOperation._( + type: DavPatchOperationType.setTaskParent, + propertyName: 'RELATED-TO', + value: parentUid?.trim(), + ); + + factory DavPatchOperation.replaceAlarm({ + required int alarmIndex, + IcalComponent? alarm, + }) { + if (alarmIndex < 0 || (alarm != null && alarm.name != 'VALARM')) { + throw ArgumentError('The alarm patch target is invalid.'); + } + return DavPatchOperation._( + type: DavPatchOperationType.replaceAlarm, + propertyName: 'VALARM', + alarmIndex: alarmIndex, + alarm: alarm?.deepCopy(), + ); + } + + factory DavPatchOperation.addComponent(IcalComponent component) { + if (component.name != 'VEVENT' && component.name != 'VTODO') { + throw ArgumentError('Only VEVENT or VTODO components can be added.'); + } + return DavPatchOperation._( + type: DavPatchOperationType.addComponent, + propertyName: 'COMPONENT-SET', + component: component.deepCopy(), + ); + } + + factory DavPatchOperation.removeComponent({IcalComponentKey? componentKey}) => + DavPatchOperation._( + type: DavPatchOperationType.removeComponent, + propertyName: 'COMPONENT-SET', + componentKey: componentKey, + ); + + factory DavPatchOperation.fromJson(Map json) { + final typeName = _requiredString(json, 'type'); + final type = DavPatchOperationType.values + .where((value) => value.name == typeName) + .firstOrNull; + if (type == null) throw _invalidPatch(); + final propertyName = _requiredString(json, 'propertyName'); + return switch (type) { + DavPatchOperationType.setText => DavPatchOperation.setText( + propertyName, + json['value'] as String?, + ), + DavPatchOperationType.setRaw => DavPatchOperation.setRaw( + propertyName, + json['value'] as String?, + parameters: _parameters(json['parameters']), + ), + DavPatchOperationType.replaceRepeatedRaw => + DavPatchOperation.replaceRepeatedRaw( + propertyName, + _mapList( + json['values'], + ).map(DavRawPropertyValue.fromJson).toList(growable: false), + ), + DavPatchOperationType.setTaskProgress => + DavPatchOperation.setTaskProgress( + _requiredInteger(json, 'percentComplete'), + completedAtUtc: _optionalDateTime(json, 'completedAtUtc'), + ), + DavPatchOperationType.setTaskParent => DavPatchOperation.setTaskParent( + json['value'] as String?, + ), + DavPatchOperationType.replaceAlarm => DavPatchOperation.replaceAlarm( + alarmIndex: _requiredInteger(json, 'alarmIndex'), + alarm: json['alarm'] == null + ? null + : _componentFromJson(_requiredMap(json, 'alarm')), + ), + DavPatchOperationType.addComponent => DavPatchOperation.addComponent( + _componentFromJson(_requiredMap(json, 'component')), + ), + DavPatchOperationType.removeComponent => + DavPatchOperation.removeComponent( + componentKey: json['componentKey'] == null + ? null + : _componentKeyFromJson(_requiredMap(json, 'componentKey')), + ), + }; + } + + final DavPatchOperationType type; + final String propertyName; + final String? value; + final List values; + final List parameters; + final int? percentComplete; + final DateTime? completedAtUtc; + final int? alarmIndex; + final IcalComponent? alarm; + final IcalComponent? component; + final IcalComponentKey? componentKey; + + Set get changedProperties => switch (type) { + DavPatchOperationType.setTaskProgress => { + 'STATUS', + 'PERCENT-COMPLETE', + 'COMPLETED', + }, + DavPatchOperationType.replaceAlarm => {'VALARM'}, + DavPatchOperationType.addComponent || + DavPatchOperationType.removeComponent => {'COMPONENT-SET'}, + _ => {propertyName}, + }; + + Map toJson() => { + 'type': type.name, + 'propertyName': propertyName, + if (value != null) 'value': value, + if (parameters.isNotEmpty) + 'parameters': [ + for (final parameter in parameters) + { + 'name': parameter.name, + 'values': parameter.values, + 'wasQuoted': parameter.wasQuoted, + }, + ], + if (type == DavPatchOperationType.replaceRepeatedRaw) + 'values': [for (final value in values) value.toJson()], + if (percentComplete != null) 'percentComplete': percentComplete, + if (completedAtUtc != null) + 'completedAtUtc': completedAtUtc!.toUtc().toIso8601String(), + if (alarmIndex != null) 'alarmIndex': alarmIndex, + if (alarm != null) 'alarm': _componentToJson(alarm!), + if (component != null) 'component': _componentToJson(component!), + if (componentKey != null) + 'componentKey': _componentKeyToJson(componentKey!), + }; +} + +final class DavMutationPatch { + DavMutationPatch({ + required this.target, + required this.scope, + required List operations, + this.schemaVersion = davMutationPatchSchemaVersion, + }) : operations = List.unmodifiable(operations) { + if (schemaVersion != davMutationPatchSchemaVersion || operations.isEmpty) { + throw _invalidPatch(); + } + } + + factory DavMutationPatch.fromJsonString(String source) { + try { + final decoded = jsonDecode(source); + if (decoded is! Map) throw _invalidPatch(); + final json = decoded.cast(); + final version = _requiredInteger(json, 'schemaVersion'); + final scopeName = _requiredString(json, 'scope'); + final scope = DavMutationScope.values + .where((value) => value.name == scopeName) + .firstOrNull; + if (scope == null) throw _invalidPatch(); + final targetJson = _requiredMap(json, 'target'); + return DavMutationPatch( + schemaVersion: version, + scope: scope, + target: IcalComponentKey( + componentType: _requiredString(targetJson, 'componentType'), + uid: _requiredString(targetJson, 'uid'), + recurrenceIdKey: targetJson['recurrenceIdKey'] as String?, + ), + operations: _mapList( + json['operations'], + ).map(DavPatchOperation.fromJson).toList(growable: false), + ); + } on DavException { + rethrow; + } on Object { + throw _invalidPatch(); + } + } + + final int schemaVersion; + final IcalComponentKey target; + final DavMutationScope scope; + final List operations; + + Set get changedProperties => { + for (final operation in operations) ...operation.changedProperties, + }; + + bool get isBroadRecurrenceMutation => changedProperties.any( + const { + 'UID', + 'DTSTART', + 'RRULE', + 'RDATE', + 'EXDATE', + 'RECURRENCE-ID', + 'COMPONENT-SET', + }.contains, + ); + + /// Freezes replay-sensitive values (currently completion timestamps) when + /// the operation is enqueued. Replaying after a restart must produce the + /// same candidate representation. + DavMutationPatch materialize(DateTime nowUtc) { + var changed = false; + final materialized = []; + for (final operation in operations) { + if (operation.type == DavPatchOperationType.setTaskProgress && + operation.percentComplete == 100 && + operation.completedAtUtc == null) { + changed = true; + materialized.add( + DavPatchOperation.setTaskProgress( + 100, + completedAtUtc: nowUtc.toUtc(), + ), + ); + } else { + materialized.add(operation); + } + } + return changed + ? DavMutationPatch( + target: target, + scope: scope, + operations: materialized, + schemaVersion: schemaVersion, + ) + : this; + } + + String toJsonString() => jsonEncode({ + 'schemaVersion': schemaVersion, + 'scope': scope.name, + 'target': { + 'componentType': target.componentType, + 'uid': target.uid, + if (target.recurrenceIdKey != null) + 'recurrenceIdKey': target.recurrenceIdKey, + }, + 'operations': [for (final operation in operations) operation.toJson()], + }); + + String applyTo(String rawIcs, {required DateTime nowUtc}) { + final document = IcalDocument.parse(rawIcs); + final patcher = IcalDocumentPatcher(document); + for (final operation in operations) { + switch (operation.type) { + case DavPatchOperationType.setText: + patcher.replaceSingletonText( + target, + operation.propertyName, + operation.value, + ); + case DavPatchOperationType.setRaw: + patcher.replaceSingletonRaw( + target, + operation.propertyName, + operation.value, + parameters: operation.parameters, + ); + case DavPatchOperationType.replaceRepeatedRaw: + patcher.replaceRepeatedRaw(target, operation.propertyName, [ + for (final value in operation.values) + (value: value.value, parameters: value.parameters), + ]); + case DavPatchOperationType.setTaskProgress: + _applyTaskProgress( + patcher, + target, + operation.percentComplete!, + operation.completedAtUtc ?? nowUtc, + ); + case DavPatchOperationType.setTaskParent: + _applyTaskParent(patcher, target, operation.value); + case DavPatchOperationType.replaceAlarm: + _applyAlarmPatch( + patcher.requireComponent(target), + operation.alarmIndex!, + operation.alarm, + ); + case DavPatchOperationType.addComponent: + patcher.addComponent(operation.component!.deepCopy()); + case DavPatchOperationType.removeComponent: + patcher.removeComponent(operation.componentKey ?? target); + } + } + final serialized = document.serialize(); + // The semantic parse is an invariant gate: no patch can emit a malformed + // recurrence set or an invalid VEVENT/VTODO resource. + IcalSemanticDocument.parse(serialized); + return serialized; + } +} + +void _applyTaskProgress( + IcalDocumentPatcher patcher, + IcalComponentKey target, + int percent, + DateTime nowUtc, +) { + if (target.componentType.toUpperCase() != 'VTODO') throw _invalidPatch(); + if (percent == 100) { + patcher + ..replaceSingletonRaw(target, 'STATUS', 'COMPLETED') + ..replaceSingletonRaw(target, 'PERCENT-COMPLETE', '100') + ..replaceSingletonRaw(target, 'COMPLETED', _utcIcal(nowUtc)); + } else if (percent == 0) { + patcher + ..replaceSingletonRaw(target, 'STATUS', 'NEEDS-ACTION') + ..replaceSingletonRaw(target, 'PERCENT-COMPLETE', null) + ..replaceSingletonRaw(target, 'COMPLETED', null); + } else { + patcher + ..replaceSingletonRaw(target, 'STATUS', 'IN-PROCESS') + ..replaceSingletonRaw(target, 'PERCENT-COMPLETE', '$percent') + ..replaceSingletonRaw(target, 'COMPLETED', null); + } +} + +void _applyTaskParent( + IcalDocumentPatcher patcher, + IcalComponentKey target, + String? parentUid, +) { + if (target.componentType.toUpperCase() != 'VTODO' || + parentUid == target.uid) { + throw _invalidPatch(); + } + final component = patcher.requireComponent(target); + final retained = []; + for (final property in component.propertiesNamed('RELATED-TO')) { + final relationType = property.parameterValue('RELTYPE')?.toUpperCase(); + if (relationType == null || relationType == 'PARENT') continue; + retained.add( + DavRawPropertyValue( + value: property.rawValue, + parameters: property.parameters, + ), + ); + } + if (parentUid != null && parentUid.isNotEmpty) { + retained.add( + DavRawPropertyValue( + value: parentUid, + parameters: const [ + IcalParameter(name: 'RELTYPE', values: ['PARENT'], wasQuoted: false), + ], + ), + ); + } + patcher.replaceRepeatedRaw(target, 'RELATED-TO', [ + for (final value in retained) + (value: value.value, parameters: value.parameters), + ]); +} + +void _applyAlarmPatch( + IcalComponent component, + int alarmIndex, + IcalComponent? alarm, +) { + final childIndexes = []; + for (var index = 0; index < component.children.length; index += 1) { + final child = component.children[index]; + if (child is IcalComponent && child.name == 'VALARM') { + childIndexes.add(index); + } + } + if (alarmIndex > childIndexes.length) throw _invalidPatch(); + if (alarmIndex == childIndexes.length) { + if (alarm == null) throw _invalidPatch(); + component.children.add(alarm.deepCopy()); + } else if (alarm == null) { + component.children.removeAt(childIndexes[alarmIndex]); + } else { + component.children[childIndexes[alarmIndex]] = alarm.deepCopy(); + } + component.structurallyDirty = true; +} + +String _utcIcal(DateTime value) { + final utc = value.toUtc(); + String two(int number) => number.toString().padLeft(2, '0'); + return '${utc.year.toString().padLeft(4, '0')}${two(utc.month)}' + '${two(utc.day)}T${two(utc.hour)}${two(utc.minute)}${two(utc.second)}Z'; +} + +const _editableProperties = { + 'SUMMARY', + 'DESCRIPTION', + 'LOCATION', + 'DTSTART', + 'DTEND', + 'DUE', + 'DURATION', + 'STATUS', + 'CLASS', + 'TRANSP', + 'URL', + 'PRIORITY', + 'PERCENT-COMPLETE', + 'COMPLETED', + 'CATEGORIES', + 'RELATED-TO', + 'RRULE', + 'RDATE', + 'EXDATE', + 'SEQUENCE', + 'CREATED', + 'DTSTAMP', + 'LAST-MODIFIED', + 'X-APPLE-SORT-ORDER', + 'X-PINNED', + 'X-OC-HIDESUBTASKS', + 'X-OC-HIDECOMPLETEDSUBTASKS', +}; + +String _validatedEditableProperty(String value) { + final upper = value.toUpperCase(); + if (!_editableProperties.contains(upper)) throw _invalidPatch(); + return upper; +} + +Map _componentToJson(IcalComponent component) => { + 'name': component.name, + 'children': [ + for (final child in component.children) + switch (child) { + final IcalProperty property => { + 'node': 'property', + 'name': property.name, + if (property.group != null) 'group': property.group, + 'value': property.rawValue, + 'parameters': [ + for (final parameter in property.parameters) + { + 'name': parameter.name, + 'values': parameter.values, + 'wasQuoted': parameter.wasQuoted, + }, + ], + }, + final IcalComponent nested => { + 'node': 'component', + 'component': _componentToJson(nested), + }, + }, + ], +}; + +Map _componentKeyToJson(IcalComponentKey key) => { + 'componentType': key.componentType.toUpperCase(), + 'uid': key.uid, + if (key.recurrenceIdKey != null) 'recurrenceIdKey': key.recurrenceIdKey, +}; + +IcalComponentKey _componentKeyFromJson(Map json) => + IcalComponentKey( + componentType: _requiredString(json, 'componentType'), + uid: _requiredString(json, 'uid'), + recurrenceIdKey: json['recurrenceIdKey'] as String?, + ); + +IcalComponent _componentFromJson(Map json) { + final name = _requiredString(json, 'name').toUpperCase(); + if (name != 'VALARM' && name != 'VEVENT' && name != 'VTODO') { + throw _invalidPatch(); + } + final children = []; + for (final child in _mapList(json['children'])) { + final node = _requiredString(child, 'node'); + if (node == 'property') { + children.add( + IcalProperty( + group: child['group'] as String?, + name: _requiredString(child, 'name').toUpperCase(), + parameters: _parameters(child['parameters']), + rawValue: _requiredString(child, 'value'), + originalPhysicalLines: const [], + isDirty: true, + ), + ); + } else if (node == 'component' && name != 'VALARM') { + final nested = _componentFromJson(_requiredMap(child, 'component')); + if (nested.name != 'VALARM') throw _invalidPatch(); + children.add(nested); + } else { + throw _invalidPatch(); + } + } + return IcalComponent( + name: name, + children: children, + originalBeginLine: 'BEGIN:$name', + originalEndLine: 'END:$name', + structurallyDirty: true, + ); +} + +List _parameters(Object? source) { + if (source == null) return const []; + return [ + for (final json in _mapList(source)) + IcalParameter( + name: _requiredString(json, 'name').toUpperCase(), + values: _stringList(json['values']), + wasQuoted: json['wasQuoted'] == true, + ), + ]; +} + +List> _mapList(Object? value) { + if (value is! List) throw _invalidPatch(); + return [ + for (final entry in value) + if (entry is Map) + entry.cast() + else + throw _invalidPatch(), + ]; +} + +Map _requiredMap(Map json, String key) { + final value = json[key]; + if (value is! Map) throw _invalidPatch(); + return value.cast(); +} + +String _requiredString(Map json, String key) { + final value = json[key]; + if (value is! String || value.isEmpty) throw _invalidPatch(); + return value; +} + +int _requiredInteger(Map json, String key) { + final value = json[key]; + if (value is! int) throw _invalidPatch(); + return value; +} + +DateTime? _optionalDateTime(Map json, String key) { + final source = json[key]; + if (source == null) return null; + if (source is! String) throw _invalidPatch(); + final value = DateTime.tryParse(source); + if (value == null || !value.isUtc) throw _invalidPatch(); + return value; +} + +List _stringList(Object? source) { + if (source is! List || source.any((value) => value is! String)) { + throw _invalidPatch(); + } + return source.cast(); +} + +DavException _invalidPatch() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavMutationPatchInvalid', + safeMessage: 'A pending calendar mutation patch was invalid.', +); diff --git a/lib/src/dav/mutation/dav_pending_operations.dart b/lib/src/dav/mutation/dav_pending_operations.dart new file mode 100644 index 0000000..b4bf855 --- /dev/null +++ b/lib/src/dav/mutation/dav_pending_operations.dart @@ -0,0 +1,1552 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../db/app_database.dart'; +import '../../features/accounts/domain/account_connection_state.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_semantics.dart'; +import '../ical/ical_timezone.dart'; +import '../storage/dav_collection_capabilities.dart'; +import '../storage/dav_object_repository.dart'; +import '../sync/dav_collection_remote_client.dart'; +import 'dav_conditional_mutation_service.dart'; +import 'dav_mutation_patch.dart'; + +const davPendingOperationSchemaVersion = 1; + +const _davMutationPutRejectedMessage = + 'The DAV server could not update the object.'; + +enum DavPendingOperationType { create, update, delete, move } + +enum DavPendingState { + pending, + retry, + inProgress, + conflict, + authBlocked, + permissionBlocked, + failed, +} + +extension on DavPendingState { + String get storageValue => switch (this) { + DavPendingState.pending => 'pending', + DavPendingState.retry => 'retry', + DavPendingState.inProgress => 'in_progress', + DavPendingState.conflict => 'conflict', + DavPendingState.authBlocked => 'auth_blocked', + DavPendingState.permissionBlocked => 'permission_blocked', + DavPendingState.failed => 'failed', + }; +} + +final class DavPendingOperationQueue { + DavPendingOperationQueue({ + required AppDatabase database, + String Function()? idFactory, + DateTime Function()? nowUtc, + }) : _database = database, + _idFactory = idFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); + + final AppDatabase _database; + final String Function() _idFactory; + final DateTime Function() _nowUtc; + + Future enqueueCreate({ + required String accountId, + required String collectionId, + required DavNewObject object, + String? localProjectionId, + String? dependsOnOperationId, + }) async { + final context = await _context(accountId, collectionId); + final capabilities = collectionCapabilitiesFromStored(context.collection); + final isEvent = _componentIsEvent(object.componentType); + final canCreate = isEvent + ? capabilities.canCreateEvent + : capabilities.canCreateTask; + if (!canCreate) throw _permissionError(); + final memberUri = _memberUri( + Uri.parse(context.collection.requestUri), + object.initialMemberName, + ); + final parsed = IcalSemanticDocument.parse(object.rawIcs); + if (parsed.primaryUid != object.uid || + parsed.components.every( + (component) => component.componentType != object.componentType, + )) { + throw _invalidPendingOperation(); + } + _validateTaskTemporalRange(parsed); + final id = _idFactory(); + final now = _nowUtc().toUtc().toIso8601String(); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: id, + accountId: accountId, + provider: Value(context.provider.storageValue), + entityType: _entityType(object.componentType), + operation: 'dav_create', + operationType: const Value('dav.create'), + davCollectionId: Value(collectionId), + davCollectionHref: Value(context.collection.hrefKey), + davMemberHref: Value(memberUri.path), + mutationPatchSchemaVersion: const Value( + davPendingOperationSchemaVersion, + ), + targetComponentKey: Value( + _componentKeyJson( + IcalComponentKey( + componentType: object.componentType.toUpperCase(), + uid: object.uid, + ), + ), + ), + mutationScope: Value(DavMutationScope.object.name), + retryClassification: const Value('conditional_create'), + localTempId: Value(object.uid), + eventId: Value(isEvent ? localProjectionId : null), + taskId: Value(isEvent ? null : localProjectionId), + dependsOnOpId: Value(dependsOnOperationId), + requestJson: jsonEncode({ + 'schemaVersion': davPendingOperationSchemaVersion, + 'uid': object.uid, + 'initialMemberName': object.initialMemberName, + 'rawIcs': object.rawIcs, + 'componentType': object.componentType.toUpperCase(), + }), + state: Value(DavPendingState.pending.storageValue), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + return id; + } + + Future enqueueUpdate({ + required String accountId, + required String collectionId, + required String objectId, + required DavMutationPatch patch, + String? dependsOnOperationId, + }) async { + final nowUtc = _nowUtc().toUtc(); + final materializedPatch = patch.materialize(nowUtc); + final context = await _objectContext(accountId, collectionId, objectId); + final capabilities = collectionCapabilitiesFromStored(context.collection); + final event = _componentIsEvent(patch.target.componentType); + if (event ? !capabilities.canUpdateEvent : !capabilities.canUpdateTask) { + throw _permissionError(); + } + final etag = context.object.etag; + if (etag == null || etag.isEmpty || context.object.serverDeleted) { + throw _invalidPendingOperation(); + } + final existing = await _activeObjectOperation(objectId); + if (existing != null) { + if (dependsOnOperationId != null && + existing.dependsOnOpId != null && + existing.dependsOnOpId != dependsOnOperationId) { + throw _operationAlreadyPending(); + } + final coalesced = _coalesceUnsentUpdate( + existing, + materializedPatch, + baselineEtag: etag, + ); + if (coalesced != null) { + // Coalesced operations are always replayed against the original + // server-confirmed baseline. Validate that exact durable candidate. + final candidate = coalesced.applyTo( + _required(existing.baselineRawIcs), + nowUtc: nowUtc, + ); + _validateTaskTemporalRange(IcalSemanticDocument.parse(candidate)); + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(existing.id))).write( + PendingOpsCompanion( + mutationPatchJson: Value(coalesced.toJsonString()), + mutationPatchSchemaVersion: Value(coalesced.schemaVersion), + dependsOnOpId: existing.dependsOnOpId == null + ? Value(dependsOnOperationId) + : const Value.absent(), + updatedAtUtc: Value(nowUtc.toIso8601String()), + ), + ); + return existing.id; + } + throw _operationAlreadyPending(); + } + + final candidate = materializedPatch.applyTo( + context.object.rawIcsBody, + nowUtc: nowUtc, + ); + _validateTaskTemporalRange(IcalSemanticDocument.parse(candidate)); + + final id = _idFactory(); + final now = nowUtc.toIso8601String(); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: id, + accountId: accountId, + provider: Value(context.provider.storageValue), + entityType: _entityType(patch.target.componentType), + operation: 'dav_update', + operationType: const Value('dav.update'), + davCollectionId: Value(collectionId), + davCollectionHref: Value(context.collection.hrefKey), + davObjectId: Value(objectId), + davMemberHref: Value(context.object.hrefKey), + baselineEtag: Value(etag), + baselineRawIcs: Value(context.object.rawIcsBody), + mutationPatchJson: Value(materializedPatch.toJsonString()), + mutationPatchSchemaVersion: Value(materializedPatch.schemaVersion), + targetComponentKey: Value(_componentKeyJson(patch.target)), + mutationScope: Value(patch.scope.name), + retryClassification: const Value('conditional_update'), + dependsOnOpId: Value(dependsOnOperationId), + requestJson: '{}', + state: Value(DavPendingState.pending.storageValue), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + return id; + } + + /// Returns the server baseline plus any provably-unsent update for local + /// editor patch construction. An operation that may have reached the server + /// is deliberately not editable until replay reconciles its outcome. + Future editableRawIcsForObject({ + required String accountId, + required String collectionId, + required String objectId, + }) async { + final context = await _objectContext(accountId, collectionId, objectId); + final existing = await _activeObjectOperation(objectId); + if (existing == null) return context.object.rawIcsBody; + final safelyEditable = + existing.operationType == 'dav.update' && + existing.state == DavPendingState.pending.storageValue && + existing.attemptCount == 0 && + existing.baselineEtag == context.object.etag && + existing.baselineRawIcs != null; + if (!safelyEditable) throw _operationAlreadyPending(); + return _decodePatch( + existing, + ).applyTo(existing.baselineRawIcs!, nowUtc: _nowUtc().toUtc()); + } + + /// Applies a typed patch to a create that is still entirely local. + /// + /// This is deliberately limited to an operation that has never been sent, + /// or one the server explicitly rejected before creating the resource. + /// Unknown-outcome creates remain immutable until replay reconciles them. + Future updateUnsentCreate({ + required String accountId, + required String collectionId, + required String localProjectionId, + required DavMutationPatch patch, + }) async { + await _context(accountId, collectionId); + final operation = await _editableCreate( + accountId: accountId, + collectionId: collectionId, + localProjectionId: localProjectionId, + ); + if (operation == null) return false; + final object = _decodeCreate(operation.requestJson); + if (patch.target.componentType.toUpperCase() != + object.componentType.toUpperCase() || + patch.target.uid != object.uid) { + throw _invalidPendingOperation(); + } + final nowUtc = _nowUtc().toUtc(); + final materialized = patch.materialize(nowUtc); + final updatedRaw = materialized.applyTo(object.rawIcs, nowUtc: nowUtc); + _validateTaskTemporalRange(IcalSemanticDocument.parse(updatedRaw)); + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(operation.id))).write( + PendingOpsCompanion( + requestJson: Value( + jsonEncode({ + 'schemaVersion': davPendingOperationSchemaVersion, + 'uid': object.uid, + 'initialMemberName': object.initialMemberName, + 'rawIcs': updatedRaw, + 'componentType': object.componentType.toUpperCase(), + }), + ), + state: Value(DavPendingState.pending.storageValue), + retryClassification: const Value('conditional_create'), + nextAttemptAtUtc: const Value(null), + lastErrorCode: const Value(null), + lastErrorMessage: const Value(null), + lastError: const Value(null), + updatedAtUtc: Value(nowUtc.toIso8601String()), + ), + ); + return true; + } + + /// Cancels a create only while it is provably unsent. + Future cancelUnsentCreate({ + required String accountId, + required String collectionId, + required String localProjectionId, + }) async { + await _context(accountId, collectionId); + final operation = await _editableCreate( + accountId: accountId, + collectionId: collectionId, + localProjectionId: localProjectionId, + ); + if (operation == null) return false; + await _database.pendingOpsDao.deleteOp(operation.id); + return true; + } + + Future enqueueDelete({ + required String accountId, + required String collectionId, + required String objectId, + required IcalComponentKey target, + DavMutationScope scope = DavMutationScope.object, + String? dependsOnOperationId, + }) async { + final context = await _objectContext(accountId, collectionId, objectId); + final capabilities = collectionCapabilitiesFromStored(context.collection); + final event = _componentIsEvent(target.componentType); + if (event ? !capabilities.canDeleteEvent : !capabilities.canDeleteTask) { + throw _permissionError(); + } + final etag = context.object.etag; + if (etag == null || etag.isEmpty || context.object.serverDeleted) { + throw _invalidPendingOperation(); + } + final existingOperation = await _activeObjectOperation(objectId); + if (existingOperation != null) { + final cancellableUpdate = + existingOperation.operationType == 'dav.update' && + existingOperation.state == DavPendingState.pending.storageValue && + existingOperation.attemptCount == 0; + if (!cancellableUpdate) throw _operationAlreadyPending(); + await _database.pendingOpsDao.deleteOp(existingOperation.id); + } + final semantic = IcalSemanticDocument.parse(context.object.rawIcsBody); + if (!_containsTarget(semantic, target)) throw _invalidPendingOperation(); + final id = _idFactory(); + final now = _nowUtc().toUtc().toIso8601String(); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: id, + accountId: accountId, + provider: Value(context.provider.storageValue), + entityType: _entityType(target.componentType), + operation: 'dav_delete', + operationType: const Value('dav.delete'), + davCollectionId: Value(collectionId), + davCollectionHref: Value(context.collection.hrefKey), + davObjectId: Value(objectId), + davMemberHref: Value(context.object.hrefKey), + baselineEtag: Value(etag), + baselineRawIcs: Value(context.object.rawIcsBody), + mutationPatchSchemaVersion: const Value( + davPendingOperationSchemaVersion, + ), + targetComponentKey: Value(_componentKeyJson(target)), + mutationScope: Value(scope.name), + retryClassification: const Value('conditional_delete'), + dependsOnOpId: Value(dependsOnOperationId), + requestJson: jsonEncode({'isEvent': event}), + state: Value(DavPendingState.pending.storageValue), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + return id; + } + + Future enqueueMove({ + required String accountId, + required String sourceCollectionId, + required String destinationCollectionId, + required String objectId, + required IcalComponentKey target, + String? localProjectionId, + DavMutationPatch? postMovePatch, + String? dependsOnOperationId, + }) async { + final source = await _objectContext( + accountId, + sourceCollectionId, + objectId, + ); + final destination = await _context(accountId, destinationCollectionId); + if (source.provider != BusyProvider.nextcloud || + destination.provider != BusyProvider.nextcloud || + source.collection.id == destination.collection.id) { + throw _invalidPendingOperation(); + } + final sourceCapabilities = collectionCapabilitiesFromStored( + source.collection, + ); + final destinationCapabilities = collectionCapabilitiesFromStored( + destination.collection, + ); + final event = _componentIsEvent(target.componentType); + final canDelete = event + ? sourceCapabilities.canDeleteEvent + : sourceCapabilities.canDeleteTask; + final canCreate = event + ? destinationCapabilities.canCreateEvent + : destinationCapabilities.canCreateTask; + final canUpdate = event + ? destinationCapabilities.canUpdateEvent + : destinationCapabilities.canUpdateTask; + if (!canDelete || + !canCreate || + (postMovePatch != null && !canUpdate) || + source.object.serverDeleted || + source.object.etag == null || + source.object.etag!.isEmpty) { + throw _permissionError(); + } + + final sourceUri = Uri.tryParse(source.object.requestUri); + final destinationCollectionUri = Uri.tryParse( + destination.collection.requestUri, + ); + if (sourceUri == null || destinationCollectionUri == null) { + throw _invalidPendingOperation(); + } + final destinationUri = _moveDestinationUri( + sourceUri, + destinationCollectionUri, + ); + final existing = await _activeObjectOperation(objectId); + var dependency = dependsOnOperationId; + var intendedSourceRaw = source.object.rawIcsBody; + if (existing != null) { + final isEditableUpdate = + existing.operationType == 'dav.update' && + existing.state == DavPendingState.pending.storageValue && + existing.attemptCount == 0 && + existing.baselineRawIcs != null; + if (!isEditableUpdate) throw _operationAlreadyPending(); + if (dependency != null && + existing.dependsOnOpId != null && + existing.dependsOnOpId != dependency) { + throw _operationAlreadyPending(); + } + if (dependency != null && existing.dependsOnOpId == null) { + await (_database.update(_database.pendingOps) + ..where((row) => row.id.equals(existing.id))) + .write(PendingOpsCompanion(dependsOnOpId: Value(dependency))); + } + intendedSourceRaw = _decodePatch( + existing, + ).applyTo(existing.baselineRawIcs!, nowUtc: _nowUtc().toUtc()); + dependency = existing.id; + } + final semantic = IcalSemanticDocument.parse(intendedSourceRaw); + if (!_containsTarget(semantic, target)) throw _invalidPendingOperation(); + final nowUtc = _nowUtc().toUtc(); + final materializedPatch = postMovePatch?.materialize(nowUtc); + if (materializedPatch != null) { + if (!_sameTarget(materializedPatch.target, target)) { + throw _invalidPendingOperation(); + } + _validateTaskTemporalRange( + IcalSemanticDocument.parse( + materializedPatch.applyTo(intendedSourceRaw, nowUtc: nowUtc), + ), + ); + } + + final id = _idFactory(); + final now = nowUtc.toIso8601String(); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: id, + accountId: accountId, + provider: Value(source.provider.storageValue), + entityType: _entityType(target.componentType), + operation: 'dav_move', + operationType: const Value('dav.move'), + davCollectionId: Value(sourceCollectionId), + davCollectionHref: Value(source.collection.hrefKey), + davObjectId: Value(objectId), + davMemberHref: Value(source.object.hrefKey), + baselineEtag: Value(source.object.etag), + baselineRawIcs: Value(intendedSourceRaw), + mutationPatchJson: Value(materializedPatch?.toJsonString()), + mutationPatchSchemaVersion: Value(materializedPatch?.schemaVersion), + targetComponentKey: Value(_componentKeyJson(target)), + mutationScope: Value( + materializedPatch?.scope.name ?? DavMutationScope.object.name, + ), + destinationCollectionId: Value(destinationCollectionId), + destinationCollectionHref: Value(destination.collection.hrefKey), + destinationMemberHref: Value(destinationUri.path), + retryClassification: const Value('conditional_move'), + taskId: Value(event ? null : localProjectionId), + eventId: Value(event ? localProjectionId : null), + dependsOnOpId: Value(dependency), + requestJson: jsonEncode({ + 'isEvent': event, + 'destinationRequestUri': destinationUri.toString(), + }), + state: Value(DavPendingState.pending.storageValue), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + return id; + } + + Future<_DavContext> _context(String accountId, String collectionId) async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(accountId))).getSingleOrNull(); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingleOrNull(); + if (account == null || + collection == null || + collection.accountId != accountId || + collection.deleted || + collection.serverMissing) { + throw _invalidPendingOperation(); + } + final provider = BusyProviderCodec.requireStorageValue(account.provider); + if (provider != BusyProvider.appleICloud && + provider != BusyProvider.nextcloud) { + throw _invalidPendingOperation(); + } + return _DavContext( + account: account, + collection: collection, + provider: provider, + ); + } + + Future<_DavObjectContext> _objectContext( + String accountId, + String collectionId, + String objectId, + ) async { + final context = await _context(accountId, collectionId); + final object = await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (object == null || + object.accountId != accountId || + object.collectionId != collectionId) { + throw _invalidPendingOperation(); + } + return _DavObjectContext( + account: context.account, + collection: context.collection, + provider: context.provider, + object: object, + ); + } + + Future _activeObjectOperation(String objectId) { + return (_database.select(_database.pendingOps) + ..where( + (row) => + row.davObjectId.equals(objectId) & + row.state.isIn(const [ + 'pending', + 'retry', + 'in_progress', + 'conflict', + 'auth_blocked', + 'permission_blocked', + ]), + ) + ..orderBy([(row) => OrderingTerm.asc(row.createdAtUtc)]) + ..limit(1)) + .getSingleOrNull(); + } + + Future _editableCreate({ + required String accountId, + required String collectionId, + required String localProjectionId, + }) { + return (_database.select(_database.pendingOps) + ..where( + (row) => + row.accountId.equals(accountId) & + row.davCollectionId.equals(collectionId) & + row.operationType.equals('dav.create') & + row.state.isIn(const ['pending', 'failed']) & + row.attemptCount.equals(0) & + (row.eventId.equals(localProjectionId) | + row.taskId.equals(localProjectionId)), + ) + ..limit(1)) + .getSingleOrNull() + .then((operation) { + if (operation == null || !isDavCreateLocallyEditable(operation)) { + return null; + } + return operation; + }); + } +} + +/// Whether a local DAV create may be rewritten or cancelled without risking a +/// duplicate remote object. +/// +/// A pending, never-attempted create is entirely local. A permanently failed +/// create is also safe only when the stored message proves that the server +/// returned an explicit rejection to the PUT itself (rather than a later GET +/// failing after the object may already have been created). +bool isDavCreateLocallyEditable(PendingOp operation) { + if (operation.operationType != 'dav.create' || operation.attemptCount != 0) { + return false; + } + if (operation.state == DavPendingState.pending.storageValue) { + return true; + } + return operation.state == DavPendingState.failed.storageValue && + operation.retryClassification == 'permanent' && + operation.lastErrorMessage == _davMutationPutRejectedMessage; +} + +typedef DavMutationServiceFactory = + Future Function({ + required Account account, + required DavCollection collection, + }); + +typedef DavPendingOperationFailureHandler = + Future Function(PendingOp operation, DavException error); + +final class DavReplaySummary { + const DavReplaySummary({ + required this.appliedCount, + required this.conflictCount, + required this.retryCount, + required this.mutatedCollectionIds, + required this.affectedObjectIds, + required this.paused, + }); + + final int appliedCount; + final int conflictCount; + final int retryCount; + final Set mutatedCollectionIds; + final Set affectedObjectIds; + final bool paused; +} + +final class DavPendingOperationsReplayer { + DavPendingOperationsReplayer({ + required AppDatabase database, + required String accountId, + required DavMutationServiceFactory serviceFactory, + DavObjectRepository? objectRepository, + Future Function(Set objectIds)? rebuildNotifications, + Future Function(Set collectionIds)? requestFollowUpSync, + DavPendingOperationFailureHandler? onPermanentFailure, + String Function()? idFactory, + DateTime Function()? nowUtc, + Random? random, + }) : _database = database, + _accountId = accountId, + _serviceFactory = serviceFactory, + _objectRepository = + objectRepository ?? DavObjectRepository(database: database), + _rebuildNotifications = rebuildNotifications, + _requestFollowUpSync = requestFollowUpSync, + _onPermanentFailure = onPermanentFailure, + _idFactory = idFactory ?? const Uuid().v4, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()), + _random = random ?? Random.secure(); + + final AppDatabase _database; + final String _accountId; + final DavMutationServiceFactory _serviceFactory; + final DavObjectRepository _objectRepository; + final Future Function(Set)? _rebuildNotifications; + final Future Function(Set)? _requestFollowUpSync; + final DavPendingOperationFailureHandler? _onPermanentFailure; + final String Function() _idFactory; + final DateTime Function() _nowUtc; + final Random _random; + + Future replayDueOperations() async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingleOrNull(); + if (account == null || !_mayReplay(account.authState)) { + return const DavReplaySummary( + appliedCount: 0, + conflictCount: 0, + retryCount: 0, + mutatedCollectionIds: {}, + affectedObjectIds: {}, + paused: true, + ); + } + final now = _nowUtc().toUtc(); + final query = _database.select(_database.pendingOps) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.state.isIn(const ['pending', 'retry', 'in_progress']) & + (row.nextAttemptAtUtc.isNull() | + row.nextAttemptAtUtc.isSmallerOrEqualValue( + now.toIso8601String(), + )), + ) + ..orderBy([(row) => OrderingTerm.asc(row.createdAtUtc)]); + final operations = _dependencyOrder( + (await query.get()).where(_isDavOperation).toList(), + ); + var applied = 0; + var conflicts = 0; + var retries = 0; + var paused = false; + final changedObjects = {}; + final changedCollections = {}; + + for (final listed in operations) { + final op = await _database.pendingOpsDao.getOp(listed.id); + if (op == null || !_isDavOperation(op)) continue; + if (op.dependsOnOpId != null && await _opExists(op.dependsOnOpId!)) { + continue; + } + try { + await _markInProgress(op); + final result = await _replay(op); + if (result.outcome == DavMutationOutcome.conflict) { + await _recordConflict(op, result); + conflicts += 1; + continue; + } + final affected = await _commitSuccess(op, result); + changedObjects.addAll(affected); + changedCollections.add(op.davCollectionId!); + if (op.destinationCollectionId != null) { + changedCollections.add(op.destinationCollectionId!); + } + applied += 1; + } on DavException catch (error) { + switch (error.category) { + case DavErrorCategory.davAuthRejected || + DavErrorCategory.davCredentialsRevoked: + await _pauseForAuthentication(op, error); + paused = true; + case DavErrorCategory.davPermissionDenied || + DavErrorCategory.davReadOnly: + await _pauseForPermission(op, error); + paused = true; + case DavErrorCategory.davResourceConflict || + DavErrorCategory.davUidConflict: + await _recordExceptionConflict(op, error); + conflicts += 1; + case DavErrorCategory.davTransientNetwork || + DavErrorCategory.davServerUnavailable || + DavErrorCategory.davRateLimited: + await _scheduleRetry(op, error); + retries += 1; + case _: + await _markFailed(op, error); + await _reportPermanentFailure(op, error); + } + if (paused) break; + } on Object { + await _scheduleRetry( + op, + const DavException( + kind: DavErrorKind.network, + code: 'DavPendingReplayUnexpectedFailure', + safeMessage: 'The pending DAV operation could not be replayed.', + ), + ); + retries += 1; + } + } + + if (changedObjects.isNotEmpty && _rebuildNotifications != null) { + await _rebuildNotifications(changedObjects); + } + if (changedCollections.isNotEmpty && _requestFollowUpSync != null) { + await _requestFollowUpSync(changedCollections); + } + return DavReplaySummary( + appliedCount: applied, + conflictCount: conflicts, + retryCount: retries, + mutatedCollectionIds: Set.unmodifiable(changedCollections), + affectedObjectIds: Set.unmodifiable(changedObjects), + paused: paused, + ); + } + + Future _replay(PendingOp op) async { + final collection = await _requiredCollection(op); + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingle(); + final capabilities = collectionCapabilitiesFromStored(collection); + final service = await _serviceFactory( + account: account, + collection: collection, + ); + final correlationId = _idFactory(); + final objectUri = op.operationType == 'dav.create' + ? null + : await _requiredObjectUri(op, collection); + final destinationCollection = op.operationType == 'dav.move' + ? await _requiredDestinationCollection(op) + : null; + final sourceObject = op.operationType == 'dav.move' + ? await _requiredObject(op, collection) + : null; + return switch (op.operationType) { + 'dav.create' => service.create( + collectionUri: Uri.parse(collection.requestUri), + object: _decodeCreate(op.requestJson), + capabilities: capabilities, + correlationId: correlationId, + ), + 'dav.update' => service.update( + hrefKey: _required(op.davMemberHref), + uri: objectUri!, + baselineEtag: _required(op.baselineEtag), + baselineRawIcs: _required(op.baselineRawIcs), + patch: _decodePatch(op), + capabilities: capabilities, + correlationId: correlationId, + ), + 'dav.delete' => service.delete( + hrefKey: _required(op.davMemberHref), + uri: objectUri!, + baselineEtag: _required(op.baselineEtag), + baselineRawIcs: _required(op.baselineRawIcs), + isEvent: _deleteIsEvent(op), + capabilities: capabilities, + correlationId: correlationId, + ), + 'dav.move' => service.move( + sourceHrefKey: _required(op.davMemberHref), + sourceUri: objectUri!, + destinationHrefKey: _required(op.destinationMemberHref), + destinationUri: _moveDestinationRequestUri(op, destinationCollection!), + baselineEtag: _required(sourceObject!.etag), + baselineRawIcs: sourceObject.rawIcsBody, + isEvent: _deleteIsEvent(op), + sourceCapabilities: capabilities, + destinationCapabilities: collectionCapabilitiesFromStored( + destinationCollection, + ), + correlationId: correlationId, + postMovePatch: _decodeOptionalMovePatch(op), + ), + _ => throw _invalidPendingOperation(), + }; + } + + Future> _commitSuccess( + PendingOp op, + DavMutationResult result, + ) async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingle(); + final provider = BusyProviderCodec.requireStorageValue(account.provider); + final canonical = result.canonicalObject; + late final Set affected; + if (op.operationType == 'dav.move') { + if (canonical == null) throw _invalidPendingOperation(); + final destination = await _requiredDestinationCollection(op); + affected = await _objectRepository.commitConfirmedMove( + accountId: _accountId, + sourceCollectionId: _required(op.davCollectionId), + destinationCollectionId: destination.id, + provider: provider, + sourceHrefKey: _required(op.davMemberHref), + canonicalDestinationObject: DavPreparedObject.parse( + hrefKey: canonical.hrefKey, + requestUri: canonical.requestUri, + etag: canonical.etag, + contentType: canonical.contentType, + rawIcsBody: _required(canonical.rawIcsBody), + maximumResourceBytes: + destination.maximumResourceSize ?? 16 * 1024 * 1024, + ), + completedAtUtc: _nowUtc(), + ); + } else { + affected = canonical == null + ? await _objectRepository.commitConfirmedMutation( + accountId: _accountId, + collectionId: _required(op.davCollectionId), + provider: provider, + deletedHrefKey: _required(op.davMemberHref), + completedAtUtc: _nowUtc(), + ) + : await _objectRepository.commitConfirmedMutation( + accountId: _accountId, + collectionId: _required(op.davCollectionId), + provider: provider, + canonicalObject: DavPreparedObject.parse( + hrefKey: canonical.hrefKey, + requestUri: canonical.requestUri, + etag: canonical.etag, + contentType: canonical.contentType, + rawIcsBody: _required(canonical.rawIcsBody), + maximumResourceBytes: + (await _requiredCollection(op)).maximumResourceSize ?? + 16 * 1024 * 1024, + ), + completedAtUtc: _nowUtc(), + ); + } + await _database.pendingOpsDao.deleteOp(op.id); + if (op.operationType == 'dav.create') { + if (op.eventId != null) { + await (_database.delete(_database.calendarEvents)..where( + (row) => + row.accountId.equals(_accountId) & + row.id.equals(op.eventId!) & + row.davObjectId.isNull(), + )) + .go(); + } + if (op.taskId != null) { + await (_database.delete(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.id.equals(op.taskId!) & + row.davObjectId.isNull(), + )) + .go(); + } + } + await _setAccountState(AccountConnectionState.connected); + return affected; + } + + Future _recordConflict(PendingOp op, DavMutationResult result) async { + final analysis = result.conflict; + if (analysis == null) throw _invalidPendingOperation(); + await _insertConflict( + op, + code: analysis.conflictCode ?? 'DavResourceConflict', + localCandidateRawIcs: result.localCandidateRawIcs ?? _localCandidate(op), + remote: result.conflictRemoteObject, + ); + } + + Future _recordExceptionConflict(PendingOp op, DavException error) => + _insertConflict( + op, + code: error.code, + localCandidateRawIcs: _localCandidate(op), + remote: null, + ); + + Future _insertConflict( + PendingOp op, { + required String code, + required String localCandidateRawIcs, + required DavFetchedMember? remote, + }) async { + final snapshotId = _idFactory(); + final now = _nowUtc().toUtc().toIso8601String(); + await _database.transaction(() async { + await _database + .into(_database.davConflictSnapshots) + .insert( + DavConflictSnapshotsCompanion.insert( + id: snapshotId, + accountId: _accountId, + davCollectionId: Value(op.davCollectionId), + davObjectId: Value(op.davObjectId), + baselineEtag: Value(op.baselineEtag), + baselineRawIcs: op.baselineRawIcs ?? '', + localCandidateRawIcs: localCandidateRawIcs, + remoteEtag: Value(remote?.etag), + remoteRawIcs: remote?.rawIcsBody ?? '', + conflictCode: code, + createdAtUtc: now, + ), + ); + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(op.id))).write( + PendingOpsCompanion( + state: Value(DavPendingState.conflict.storageValue), + conflictState: const Value('unresolved'), + conflictSnapshotId: Value(snapshotId), + retryClassification: const Value('manual_conflict_resolution'), + nextAttemptAtUtc: const Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: Value(code), + lastErrorMessage: const Value( + 'A remote edit conflicts with this change.', + ), + updatedAtUtc: Value(now), + ), + ); + }); + } + + Future _pauseForAuthentication(PendingOp op, DavException error) async { + await _setAccountState(AccountConnectionState.reauthenticationRequired); + await _block( + op, + state: DavPendingState.authBlocked, + classification: 'authentication', + error: error, + ); + } + + Future _pauseForPermission(PendingOp op, DavException error) async { + await _setAccountState(AccountConnectionState.permissionChanged); + if (op.davCollectionId != null) { + await (_database.update( + _database.davCollections, + )..where((row) => row.id.equals(op.davCollectionId!))).write( + DavCollectionsCompanion( + readOnly: const Value(true), + updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()), + ), + ); + } + await _block( + op, + state: DavPendingState.permissionBlocked, + classification: 'permission', + error: error, + ); + } + + Future _markFailed(PendingOp op, DavException error) => _block( + op, + state: DavPendingState.failed, + classification: 'permanent', + error: error, + ); + + Future _reportPermanentFailure( + PendingOp operation, + DavException error, + ) async { + try { + await _onPermanentFailure?.call(operation, error); + } on Object { + // Reporting must not change the durable mutation outcome. + } + } + + Future _block( + PendingOp op, { + required DavPendingState state, + required String classification, + required DavException error, + }) { + final now = _nowUtc().toUtc().toIso8601String(); + return (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(op.id))).write( + PendingOpsCompanion( + state: Value(state.storageValue), + retryClassification: Value(classification), + nextAttemptAtUtc: const Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: Value(error.code), + lastErrorMessage: Value(error.safeMessage), + updatedAtUtc: Value(now), + ), + ); + } + + Future _scheduleRetry(PendingOp op, DavException error) async { + final attempt = op.attemptCount + 1; + final exponentialSeconds = min(3600, 1 << min(attempt, 11)); + final jitterMilliseconds = _random.nextInt(1000); + final delay = + error.retryAfter ?? + Duration(seconds: exponentialSeconds, milliseconds: jitterMilliseconds); + final now = _nowUtc().toUtc(); + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(op.id))).write( + PendingOpsCompanion( + state: Value(DavPendingState.retry.storageValue), + attemptCount: Value(attempt), + nextAttemptAtUtc: Value(now.add(delay).toIso8601String()), + retryClassification: const Value('transient'), + lastErrorCode: Value(error.code), + lastErrorMessage: Value(error.safeMessage), + updatedAtUtc: Value(now.toIso8601String()), + ), + ); + await _setAccountState(AccountConnectionState.temporarilyUnavailable); + } + + Future _markInProgress(PendingOp op) { + return (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(op.id))).write( + PendingOpsCompanion( + state: Value(DavPendingState.inProgress.storageValue), + nextAttemptAtUtc: const Value(null), + updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()), + ), + ); + } + + Future _setAccountState(AccountConnectionState state) { + return (_database.update( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).write( + AccountsCompanion( + authState: Value(state.storageValue), + updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()), + ), + ); + } + + Future _opExists(String id) async => + await _database.pendingOpsDao.getOp(id) != null; + + Future _requiredCollection(PendingOp op) async { + final id = _required(op.davCollectionId); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(id))).getSingleOrNull(); + if (collection == null || + collection.accountId != _accountId || + collection.deleted || + collection.serverMissing) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavCollectionRemoved', + safeMessage: 'The DAV collection is no longer available.', + ); + } + return collection; + } + + Future _requiredDestinationCollection(PendingOp op) async { + final id = _required(op.destinationCollectionId); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(id))).getSingleOrNull(); + if (collection == null || + collection.accountId != _accountId || + collection.deleted || + collection.serverMissing || + collection.hrefKey != op.destinationCollectionHref) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavMoveDestinationRemoved', + safeMessage: 'The destination DAV collection is no longer available.', + ); + } + return collection; + } + + Future _requiredObject( + PendingOp op, + DavCollection collection, + ) async { + final objectId = _required(op.davObjectId); + final object = await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (object == null || + object.accountId != _accountId || + object.collectionId != collection.id || + object.hrefKey != op.davMemberHref || + object.serverDeleted) { + throw _invalidPendingOperation(); + } + return object; + } + + Future _requiredObjectUri(PendingOp op, DavCollection collection) async { + final href = _required(op.davMemberHref); + final collectionHref = _required(op.davCollectionHref); + if (!href.startsWith(collectionHref)) throw _invalidPendingOperation(); + final object = await _requiredObject(op, collection); + // The canonical request URI is persisted with the raw baseline and avoids + // reconstructing a potentially path-prefixed Nextcloud installation URL. + final uri = Uri.tryParse(object.requestUri); + final collectionUri = Uri.tryParse(collection.requestUri); + if (uri == null || + collectionUri == null || + uri.scheme != collectionUri.scheme || + uri.host != collectionUri.host || + uri.port != collectionUri.port || + uri.userInfo.isNotEmpty || + uri.hasQuery || + uri.hasFragment || + uri.path != href) { + throw _invalidPendingOperation(); + } + return uri; + } + + Uri _moveDestinationRequestUri(PendingOp op, DavCollection destination) { + try { + final decoded = jsonDecode(op.requestJson); + if (decoded is! Map) throw _invalidPendingOperation(); + final raw = decoded['destinationRequestUri']; + if (raw is! String || raw.isEmpty) throw _invalidPendingOperation(); + final uri = Uri.parse(raw); + final collectionUri = Uri.parse(destination.requestUri); + final href = _required(op.destinationMemberHref); + if (uri.scheme != collectionUri.scheme || + uri.host != collectionUri.host || + uri.port != collectionUri.port || + uri.userInfo.isNotEmpty || + uri.hasQuery || + uri.hasFragment || + uri.path != href || + !_hrefIsDirectMember(href, destination.hrefKey)) { + throw _invalidPendingOperation(); + } + return uri; + } on DavException { + rethrow; + } on Object { + throw _invalidPendingOperation(); + } + } + + String _localCandidate(PendingOp op) { + if (op.operationType == 'dav.create') { + return _decodeCreate(op.requestJson).rawIcs; + } + if (op.operationType == 'dav.update') { + return _decodePatch( + op, + ).applyTo(_required(op.baselineRawIcs), nowUtc: _nowUtc().toUtc()); + } + if (op.operationType == 'dav.move') { + return _moveCandidateRaw(op, _nowUtc().toUtc()); + } + return op.baselineRawIcs ?? ''; + } +} + +class _DavContext { + const _DavContext({ + required this.account, + required this.collection, + required this.provider, + }); + + final Account account; + final DavCollection collection; + final BusyProvider provider; +} + +final class _DavObjectContext extends _DavContext { + const _DavObjectContext({ + required super.account, + required super.collection, + required super.provider, + required this.object, + }); + + final DavObject object; +} + +void _validateTaskTemporalRange(IcalSemanticDocument document) { + IcalTimeZoneResolver? resolver; + for (final component in document.components) { + if (component.componentType != 'VTODO') { + continue; + } + final start = component.start; + final due = component.due; + if (start == null || due == null) { + continue; + } + if (start.isDate != due.isDate) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavTaskTemporalTypeMismatch', + safeMessage: + 'Task start and due values must both be all-day or both include ' + 'a time.', + ); + } + final effectiveResolver = resolver ??= IcalTimeZoneResolver.fromDocument( + document, + ); + if (effectiveResolver.toUtc(due).isBefore(effectiveResolver.toUtc(start))) { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavTaskDueBeforeStart', + safeMessage: 'A task cannot be due before it starts.', + ); + } + } +} + +DavMutationPatch? _coalesceUnsentUpdate( + PendingOp existing, + DavMutationPatch patch, { + required String baselineEtag, +}) { + if (existing.operationType != 'dav.update' || + existing.state != DavPendingState.pending.storageValue || + existing.attemptCount != 0 || + existing.baselineEtag != baselineEtag || + existing.mutationPatchJson == null) { + return null; + } + final previous = DavMutationPatch.fromJsonString(existing.mutationPatchJson!); + if (!_sameTarget(previous.target, patch.target)) { + return null; + } + final sameScope = previous.scope == patch.scope; + final editsLocallyAddedOccurrence = + previous.scope == DavMutationScope.occurrence && + patch.scope == DavMutationScope.recurrenceException && + previous.operations.any( + (operation) => operation.type == DavPatchOperationType.addComponent, + ); + if (!sameScope && !editsLocallyAddedOccurrence) return null; + return DavMutationPatch( + target: patch.target, + // A detached exception that has not been sent is still one occurrence + // mutation. Folding its subsequent property edits into that add operation + // does not merge independent recurrence scopes. + scope: previous.scope, + operations: [...previous.operations, ...patch.operations], + ); +} + +bool _sameTarget(IcalComponentKey left, IcalComponentKey right) => + left.componentType == right.componentType && + left.uid == right.uid && + left.recurrenceIdKey == right.recurrenceIdKey; + +bool _containsTarget(IcalSemanticDocument document, IcalComponentKey target) => + document.components.any( + (component) => + component.componentType == target.componentType.toUpperCase() && + component.uid == target.uid && + component.recurrenceIdKey == target.recurrenceIdKey, + ); + +DavMutationPatch _decodePatch(PendingOp op) { + if (op.mutationPatchSchemaVersion != davMutationPatchSchemaVersion || + op.mutationPatchJson == null) { + throw _invalidPendingOperation(); + } + final patch = DavMutationPatch.fromJsonString(op.mutationPatchJson!); + if (op.targetComponentKey != _componentKeyJson(patch.target) || + op.mutationScope != patch.scope.name) { + throw _invalidPendingOperation(); + } + return patch; +} + +DavMutationPatch? _decodeOptionalMovePatch(PendingOp op) { + if (op.operationType != 'dav.move') throw _invalidPendingOperation(); + if (op.mutationPatchJson == null) return null; + return _decodePatch(op); +} + +String _moveCandidateRaw(PendingOp op, DateTime nowUtc) { + final baseline = _required(op.baselineRawIcs); + final patch = _decodeOptionalMovePatch(op); + return patch == null + ? baseline + : patch.applyTo(baseline, nowUtc: nowUtc.toUtc()); +} + +DavNewObject _decodeCreate(String source) { + try { + final decoded = jsonDecode(source); + if (decoded is! Map) throw _invalidPendingOperation(); + final json = decoded.cast(); + if (json['schemaVersion'] != davPendingOperationSchemaVersion) { + throw _invalidPendingOperation(); + } + final uid = _jsonString(json, 'uid'); + final rawIcs = _jsonString(json, 'rawIcs'); + final componentType = _jsonString(json, 'componentType').toUpperCase(); + final semantic = IcalSemanticDocument.parse(rawIcs); + if (semantic.primaryUid != uid || + semantic.components.every( + (component) => component.componentType != componentType, + )) { + throw _invalidPendingOperation(); + } + return DavNewObject( + uid: uid, + initialMemberName: _jsonString(json, 'initialMemberName'), + rawIcs: rawIcs, + componentType: componentType, + ); + } on DavException { + rethrow; + } on Object { + throw _invalidPendingOperation(); + } +} + +bool _deleteIsEvent(PendingOp op) { + try { + final decoded = jsonDecode(op.requestJson); + if (decoded is! Map || decoded['isEvent'] is! bool) { + throw _invalidPendingOperation(); + } + return decoded['isEvent']! as bool; + } on DavException { + rethrow; + } on Object { + throw _invalidPendingOperation(); + } +} + +String _componentKeyJson(IcalComponentKey key) => jsonEncode({ + 'componentType': key.componentType.toUpperCase(), + 'uid': key.uid, + if (key.recurrenceIdKey != null) 'recurrenceIdKey': key.recurrenceIdKey, +}); + +String _entityType(String componentType) => + _componentIsEvent(componentType) ? 'event' : 'task'; + +bool _componentIsEvent(String componentType) => + switch (componentType.toUpperCase()) { + 'VEVENT' => true, + 'VTODO' => false, + _ => throw _invalidPendingOperation(), + }; + +bool _isDavOperation(PendingOp op) => + op.operationType == 'dav.create' || + op.operationType == 'dav.update' || + op.operationType == 'dav.delete' || + op.operationType == 'dav.move'; + +List _dependencyOrder(List source) { + final remaining = [...source]; + final ordered = []; + while (remaining.isNotEmpty) { + final remainingIds = {for (final operation in remaining) operation.id}; + final index = remaining.indexWhere( + (operation) => + operation.dependsOnOpId == null || + !remainingIds.contains(operation.dependsOnOpId), + ); + if (index < 0) { + // A corrupt cycle remains blocked by the durable dependency checks and + // will not be sent out of order. + ordered.addAll(remaining); + break; + } + ordered.add(remaining.removeAt(index)); + } + return ordered; +} + +bool _mayReplay(String storageState) { + final state = AccountConnectionStateCodec.parse(storageState); + return state == AccountConnectionState.connected || + state == AccountConnectionState.temporarilyUnavailable; +} + +String _required(String? value) { + if (value == null || value.isEmpty) throw _invalidPendingOperation(); + return value; +} + +String _jsonString(Map json, String key) { + final value = json[key]; + if (value is! String || value.isEmpty) throw _invalidPendingOperation(); + return value; +} + +Uri _memberUri(Uri collectionUri, String memberName) { + if (!RegExp(r'^[A-Za-z0-9-]+[.]ics$').hasMatch(memberName)) { + throw _invalidPendingOperation(); + } + final base = collectionUri.path.endsWith('/') + ? collectionUri + : collectionUri.replace(path: '${collectionUri.path}/'); + return base.resolve(memberName); +} + +Uri _moveDestinationUri(Uri sourceUri, Uri destinationCollectionUri) { + if (sourceUri.scheme != destinationCollectionUri.scheme || + sourceUri.host != destinationCollectionUri.host || + sourceUri.port != destinationCollectionUri.port || + sourceUri.userInfo.isNotEmpty || + destinationCollectionUri.userInfo.isNotEmpty || + sourceUri.hasQuery || + destinationCollectionUri.hasQuery || + sourceUri.hasFragment || + destinationCollectionUri.hasFragment || + sourceUri.pathSegments.isEmpty) { + throw _invalidPendingOperation(); + } + final memberName = sourceUri.pathSegments.last; + if (memberName.isEmpty || + memberName == '.' || + memberName == '..' || + memberName.contains('/')) { + throw _invalidPendingOperation(); + } + final base = destinationCollectionUri.toString().endsWith('/') + ? destinationCollectionUri.toString() + : '${destinationCollectionUri.toString()}/'; + final destination = Uri.parse('$base${Uri.encodeComponent(memberName)}'); + if (!_hrefIsDirectMember(destination.path, destinationCollectionUri.path)) { + throw _invalidPendingOperation(); + } + return destination; +} + +bool _hrefIsDirectMember(String memberHref, String collectionHref) { + final prefix = collectionHref.endsWith('/') + ? collectionHref + : '$collectionHref/'; + if (!memberHref.startsWith(prefix)) return false; + final relative = memberHref.substring(prefix.length); + return relative.isNotEmpty && !relative.contains('/'); +} + +DavException _invalidPendingOperation() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavPendingOperationInvalid', + safeMessage: 'A pending DAV operation was invalid.', +); + +DavException _operationAlreadyPending() => const DavException( + kind: DavErrorKind.conflict, + code: 'DavPendingMutationAlreadyExists', + safeMessage: 'This DAV object already has a pending change.', +); + +DavException _permissionError() => const DavException( + kind: DavErrorKind.authorization, + code: 'DavReadOnly', + safeMessage: 'This DAV collection does not allow that change.', +); diff --git a/lib/src/dav/mutation/dav_projection_mutations.dart b/lib/src/dav/mutation/dav_projection_mutations.dart new file mode 100644 index 0000000..b8c6ed2 --- /dev/null +++ b/lib/src/dav/mutation/dav_projection_mutations.dart @@ -0,0 +1,1526 @@ +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_recurrence.dart'; +import '../ical/ical_semantics.dart'; +import '../ical/ical_task_alarm.dart'; +import 'dav_conditional_mutation_service.dart'; +import 'dav_mutation_patch.dart'; + +final class DavEventMutationInput { + const DavEventMutationInput({ + required this.title, + required this.allDay, + required this.start, + required this.end, + this.startTimeZone, + this.endTimeZone, + this.description, + this.location, + this.recurrence, + this.recurrenceChanged = false, + this.reminders, + this.categories = const [], + this.categoriesChanged = false, + this.classification, + this.transparency, + }); + + final String title; + final bool allDay; + final DateTime start; + final DateTime end; + final String? startTimeZone; + final String? endTimeZone; + final String? description; + final String? location; + final Object? recurrence; + final bool recurrenceChanged; + final Object? reminders; + final List categories; + final bool categoriesChanged; + final String? classification; + final String? transparency; +} + +DavNewObject buildDavEventObject( + DavEventMutationInput input, { + String Function()? idFactory, + DateTime Function()? nowUtc, +}) { + final start = _eventTemporal( + input.start, + allDay: input.allDay, + timeZone: input.startTimeZone, + ); + final end = _eventTemporal( + input.end, + allDay: input.allDay, + timeZone: input.endTimeZone ?? input.startTimeZone, + ); + final factory = DavNewObjectFactory(idFactory: idFactory, nowUtc: nowUtc); + final initial = factory.event( + summary: input.title.trim(), + startRaw: start.value, + startParameters: start.parameters, + endRaw: end.value, + endParameters: end.parameters, + description: _nonEmpty(input.description), + location: _nonEmpty(input.location), + ); + final operations = [ + if (_classification(input.classification) case final value?) + DavPatchOperation.setRaw('CLASS', value), + if (_transparency(input.transparency) case final value?) + DavPatchOperation.setRaw('TRANSP', value), + if (input.categories.isNotEmpty) + DavPatchOperation.setRaw('CATEGORIES', _categories(input.categories)), + ..._recurrenceOperations(input.recurrence), + ..._alarmOperations(reminderMinutes(input.reminders), startIndex: 0), + ]; + if (operations.isEmpty) return initial; + final patch = DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: initial.uid), + scope: DavMutationScope.object, + operations: operations, + ); + return DavNewObject( + uid: initial.uid, + initialMemberName: initial.initialMemberName, + rawIcs: patch.applyTo( + initial.rawIcs, + nowUtc: (nowUtc ?? (() => DateTime.now().toUtc()))(), + ), + componentType: initial.componentType, + ); +} + +DavMutationPatch? buildDavEventUpdatePatch({ + required IcalComponentKey target, + required String baselineRawIcs, + required DavEventMutationInput input, +}) { + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final current = semantic.components.firstWhere( + (component) => + component.componentType == target.componentType && + component.uid == target.uid && + component.recurrenceIdKey == target.recurrenceIdKey, + ); + final operations = []; + if ((current.summary ?? '') != input.title.trim()) { + operations.add(DavPatchOperation.setText('SUMMARY', input.title.trim())); + } + if ((current.description ?? '') != (input.description ?? '')) { + operations.add( + DavPatchOperation.setText('DESCRIPTION', _nonEmpty(input.description)), + ); + } + if ((current.location ?? '') != (input.location ?? '')) { + operations.add( + DavPatchOperation.setText('LOCATION', _nonEmpty(input.location)), + ); + } + final start = _eventTemporal( + input.start, + allDay: input.allDay, + timeZone: input.startTimeZone, + ); + final end = _eventTemporal( + input.end, + allDay: input.allDay, + timeZone: input.endTimeZone ?? input.startTimeZone, + ); + if (!_sameTemporal(current.start, start)) { + operations.add( + DavPatchOperation.setRaw( + 'DTSTART', + start.value, + parameters: start.parameters, + ), + ); + } + if (!_sameTemporal(current.end, end) || current.duration != null) { + operations + ..add( + DavPatchOperation.setRaw( + 'DTEND', + end.value, + parameters: end.parameters, + ), + ) + ..add(DavPatchOperation.setRaw('DURATION', null)); + } + final classification = _classification(input.classification); + if ((current.classification ?? '') != (classification ?? '')) { + operations.add(DavPatchOperation.setRaw('CLASS', classification)); + } + final transparency = _transparency(input.transparency); + if ((current.transparency ?? '') != (transparency ?? '')) { + operations.add(DavPatchOperation.setRaw('TRANSP', transparency)); + } + if (input.categoriesChanged) { + operations.add( + DavPatchOperation.setRaw( + 'CATEGORIES', + input.categories.isEmpty ? null : _categories(input.categories), + ), + ); + } + if (input.recurrenceChanged) { + operations.addAll(_recurrenceOperations(input.recurrence)); + } + final desiredReminders = reminderMinutes(input.reminders); + final editableAlarm = _editableDisplayAlarm(current); + if (!_sameIntegers(editableAlarm.minutes, desiredReminders)) { + if (editableAlarm.index != null) { + operations.add( + DavPatchOperation.replaceAlarm( + alarmIndex: editableAlarm.index!, + alarm: desiredReminders.isEmpty + ? null + : _displayAlarm(desiredReminders.first), + ), + ); + for (var index = 1; index < desiredReminders.length; index += 1) { + operations.add( + DavPatchOperation.replaceAlarm( + alarmIndex: current.alarms.length + index - 1, + alarm: _displayAlarm(desiredReminders[index]), + ), + ); + } + } else { + operations.addAll( + _alarmOperations(desiredReminders, startIndex: current.alarms.length), + ); + } + } + if (operations.isEmpty) return null; + return DavMutationPatch( + target: target, + scope: target.recurrenceIdKey == null + ? DavMutationScope.recurrenceMaster + : DavMutationScope.recurrenceException, + operations: operations, + ); +} + +/// Creates a detached exception for one generated occurrence while retaining +/// the master, sibling exceptions, VTIMEZONEs, alarms, and unknown content in +/// the same calendar-object resource. +DavMutationPatch buildDavEventOccurrenceExceptionPatch({ + required String uid, + required String occurrenceKey, + required String baselineRawIcs, + required DavEventMutationInput input, + DateTime Function()? nowUtc, +}) { + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final master = semantic.components.where( + (component) => + component.componentType == 'VEVENT' && + component.uid == uid && + component.recurrenceIdKey == null, + ); + if (master.length != 1) throw _invalidMutation(); + final recurrence = _recurrenceIdentity(occurrenceKey); + if (semantic.components.any( + (component) => + component.componentType == 'VEVENT' && + component.uid == uid && + component.recurrenceIdKey == recurrence.key, + )) { + throw _invalidMutation(); + } + final timestamp = (nowUtc ?? (() => DateTime.now().toUtc()))().toUtc(); + final start = _eventTemporal( + input.start, + allDay: input.allDay, + timeZone: input.startTimeZone, + ); + final end = _eventTemporal( + input.end, + allDay: input.allDay, + timeZone: input.endTimeZone ?? input.startTimeZone, + ); + final sequence = master.single.sequence; + final component = IcalComponent( + name: 'VEVENT', + children: [ + _property('UID', uid), + _property( + 'RECURRENCE-ID', + recurrence.raw, + parameters: recurrence.parameters, + ), + _property('DTSTAMP', _utcIcal(timestamp)), + if (sequence != null) _property('SEQUENCE', '${sequence + 1}'), + _property('DTSTART', start.value, parameters: start.parameters), + _property('DTEND', end.value, parameters: end.parameters), + _property('SUMMARY', encodeIcalText(input.title.trim())), + if (_nonEmpty(input.description) case final description?) + _property('DESCRIPTION', encodeIcalText(description)), + if (_nonEmpty(input.location) case final location?) + _property('LOCATION', encodeIcalText(location)), + if (_classification(input.classification) case final value?) + _property('CLASS', value), + if (_transparency(input.transparency) case final value?) + _property('TRANSP', value), + if (input.categories.isNotEmpty) + _property('CATEGORIES', _categories(input.categories)), + for (final minutes in reminderMinutes(input.reminders)) + _displayAlarm(minutes), + ], + originalBeginLine: 'BEGIN:VEVENT', + originalEndLine: 'END:VEVENT', + structurallyDirty: true, + ); + return DavMutationPatch( + target: IcalComponentKey( + componentType: 'VEVENT', + uid: uid, + recurrenceIdKey: recurrence.key, + ), + scope: DavMutationScope.occurrence, + operations: [DavPatchOperation.addComponent(component)], + ); +} + +/// Cancels exactly one generated or overridden recurrence instance. A new +/// detached exception is added when necessary; existing exceptions are +/// patched in place. +DavMutationPatch buildDavEventOccurrenceCancellationPatch({ + required String uid, + required String occurrenceKey, + required String baselineRawIcs, + DateTime Function()? nowUtc, +}) { + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final recurrence = _recurrenceIdentity(occurrenceKey); + final existing = semantic.components.where( + (component) => + component.componentType == 'VEVENT' && + component.uid == uid && + component.recurrenceIdKey == recurrence.key, + ); + final target = IcalComponentKey( + componentType: 'VEVENT', + uid: uid, + recurrenceIdKey: recurrence.key, + ); + if (existing.length > 1) throw _invalidMutation(); + if (existing.length == 1) { + return DavMutationPatch( + target: target, + scope: DavMutationScope.occurrence, + operations: [DavPatchOperation.setRaw('STATUS', 'CANCELLED')], + ); + } + final master = semantic.components.where( + (component) => + component.componentType == 'VEVENT' && + component.uid == uid && + component.recurrenceIdKey == null, + ); + if (master.length != 1) throw _invalidMutation(); + final timestamp = (nowUtc ?? (() => DateTime.now().toUtc()))().toUtc(); + final sequence = master.single.sequence; + final component = IcalComponent( + name: 'VEVENT', + children: [ + _property('UID', uid), + _property( + 'RECURRENCE-ID', + recurrence.raw, + parameters: recurrence.parameters, + ), + _property('DTSTAMP', _utcIcal(timestamp)), + if (sequence != null) _property('SEQUENCE', '${sequence + 1}'), + _property('DTSTART', recurrence.raw, parameters: recurrence.parameters), + _property('STATUS', 'CANCELLED'), + ], + originalBeginLine: 'BEGIN:VEVENT', + originalEndLine: 'END:VEVENT', + structurallyDirty: true, + ); + return DavMutationPatch( + target: target, + scope: DavMutationScope.occurrence, + operations: [DavPatchOperation.addComponent(component)], + ); +} + +DavMutationPatch buildDavComponentRemovalPatch({ + required List targets, + required DavMutationScope scope, +}) { + if (targets.isEmpty) throw _invalidMutation(); + return DavMutationPatch( + target: targets.first, + scope: scope, + operations: [ + for (final target in targets) + DavPatchOperation.removeComponent(componentKey: target), + ], + ); +} + +DavNewObject buildDavTaskObject( + Map fields, { + String? parentUid, + String Function()? idFactory, + DateTime Function()? nowUtc, +}) { + final factory = DavNewObjectFactory(idFactory: idFactory, nowUtc: nowUtc); + final due = _taskTemporal(fields, prefix: 'Due'); + final initial = factory.task( + summary: fields['title']?.toString().trim() ?? '', + description: _nonEmpty(fields['notes']?.toString()), + dueRaw: due?.value, + dueParameters: due?.parameters ?? const [], + ); + final target = IcalComponentKey(componentType: 'VTODO', uid: initial.uid); + final initialComponent = IcalSemanticDocument.parse( + initial.rawIcs, + ).components.single; + final operations = [ + if (_taskTemporal(fields, prefix: 'Start') case final start?) + DavPatchOperation.setRaw( + 'DTSTART', + start.value, + parameters: start.parameters, + ), + if (fields['categories'] case final List values when values.isNotEmpty) + DavPatchOperation.setRaw( + 'CATEGORIES', + _categories(values.map((value) => value.toString())), + ), + if (_taskPriority(fields) case final priority?) + DavPatchOperation.setRaw('PRIORITY', '$priority'), + if (fields.containsKey('location')) + DavPatchOperation.setText( + 'LOCATION', + _nonEmpty(fields['location']?.toString()), + ), + if (fields.containsKey('taskUrl')) + DavPatchOperation.setRaw('URL', _taskUrl(fields['taskUrl'])), + if (fields.containsKey('taskClassification')) + DavPatchOperation.setRaw( + 'CLASS', + _classification(fields['taskClassification']?.toString()), + ), + if (fields.containsKey('taskPinned')) + DavPatchOperation.setRaw( + 'X-PINNED', + fields['taskPinned'] == true ? 'true' : null, + ), + if (fields.containsKey('taskHideSubtasks')) + DavPatchOperation.setRaw( + 'X-OC-HIDESUBTASKS', + fields['taskHideSubtasks'] == true ? '1' : '0', + ), + if (fields.containsKey('taskHideCompletedSubtasks')) + DavPatchOperation.setRaw( + 'X-OC-HIDECOMPLETEDSUBTASKS', + fields['taskHideCompletedSubtasks'] == true ? '1' : '0', + ), + if (fields['sortOrder'] is int) + DavPatchOperation.setRaw('X-APPLE-SORT-ORDER', '${fields['sortOrder']}'), + if (parentUid != null) DavPatchOperation.setTaskParent(parentUid), + ..._taskStateOperations(fields, current: initialComponent), + ..._taskRecurrenceOperations(fields['recurrence']), + if (fields.containsKey('taskAlarms')) + ..._taskAlarmOperations(initialComponent, fields['taskAlarms']) + else if (_taskReminder(fields) case final alarm?) + DavPatchOperation.replaceAlarm(alarmIndex: 0, alarm: alarm), + ]; + if (operations.isEmpty) return initial; + final patch = DavMutationPatch( + target: target, + scope: DavMutationScope.object, + operations: operations, + ); + return DavNewObject( + uid: initial.uid, + initialMemberName: initial.initialMemberName, + rawIcs: patch.applyTo( + initial.rawIcs, + nowUtc: (nowUtc ?? (() => DateTime.now().toUtc()))(), + ), + componentType: initial.componentType, + ); +} + +DavMutationPatch? buildDavTaskUpdatePatch({ + required IcalComponentKey target, + required String baselineRawIcs, + required Map fields, + String? parentUid, + DateTime Function()? nowUtc, +}) { + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final component = semantic.components.firstWhere( + (candidate) => + candidate.componentType == target.componentType && + candidate.uid == target.uid && + candidate.recurrenceIdKey == target.recurrenceIdKey, + ); + final operations = []; + if (fields.containsKey('title')) { + operations.add( + DavPatchOperation.setText( + 'SUMMARY', + fields['title']?.toString().trim() ?? '', + ), + ); + } + if (fields.containsKey('notes')) { + operations + ..add( + DavPatchOperation.setText( + 'DESCRIPTION', + _nonEmpty(fields['notes']?.toString()), + ), + ) + ..add(DavPatchOperation.setRaw('X-ALT-DESC', null)); + } + if (_containsTaskTemporal(fields, prefix: 'Due')) { + final due = _taskTemporal(fields, prefix: 'Due'); + operations.add( + DavPatchOperation.setRaw( + 'DUE', + due?.value, + parameters: due?.parameters ?? const [], + ), + ); + } + if (_containsTaskTemporal(fields, prefix: 'Start')) { + final start = _taskTemporal(fields, prefix: 'Start'); + operations.add( + DavPatchOperation.setRaw( + 'DTSTART', + start?.value, + parameters: start?.parameters ?? const [], + ), + ); + } + operations.addAll(_taskStateOperations(fields, current: component)); + if (fields.containsKey('importance') || fields.containsKey('icalPriority')) { + final priority = _taskPriority(fields); + operations.add( + DavPatchOperation.setRaw( + 'PRIORITY', + priority == null ? null : '$priority', + ), + ); + } + if (fields.containsKey('location')) { + operations.add( + DavPatchOperation.setText( + 'LOCATION', + _nonEmpty(fields['location']?.toString()), + ), + ); + } + if (fields.containsKey('taskUrl')) { + operations.add( + DavPatchOperation.setRaw('URL', _taskUrl(fields['taskUrl'])), + ); + } + if (fields.containsKey('taskClassification')) { + operations.add( + DavPatchOperation.setRaw( + 'CLASS', + _classification(fields['taskClassification']?.toString()), + ), + ); + } + if (fields.containsKey('taskPinned')) { + operations.add( + DavPatchOperation.setRaw( + 'X-PINNED', + fields['taskPinned'] == true ? 'true' : null, + ), + ); + } + if (fields.containsKey('taskHideSubtasks')) { + operations.add( + DavPatchOperation.setRaw( + 'X-OC-HIDESUBTASKS', + fields['taskHideSubtasks'] == true ? '1' : '0', + ), + ); + } + if (fields.containsKey('taskHideCompletedSubtasks')) { + operations.add( + DavPatchOperation.setRaw( + 'X-OC-HIDECOMPLETEDSUBTASKS', + fields['taskHideCompletedSubtasks'] == true ? '1' : '0', + ), + ); + } + if (fields.containsKey('sortOrder')) { + final value = fields['sortOrder']; + if (value != null && value is! int) throw _invalidMutation(); + operations.add( + DavPatchOperation.setRaw( + 'X-APPLE-SORT-ORDER', + value == null ? null : '$value', + ), + ); + } + if (fields.containsKey('categories')) { + final values = switch (fields['categories']) { + final List values => values.map((value) => value.toString()).toList(), + _ => const [], + }; + operations.add(_taskCategoriesOperation(component, values)); + } + if (fields.containsKey('parentUid')) { + operations.add(DavPatchOperation.setTaskParent(parentUid)); + } + if (fields.containsKey('recurrence')) { + operations.addAll(_taskRecurrenceOperations(fields['recurrence'])); + } + if (fields.containsKey('taskAlarms')) { + operations.addAll(_taskAlarmOperations(component, fields['taskAlarms'])); + } else if (fields.containsKey('microsoftIsReminderOn') || + fields.containsKey('microsoftReminderDateTime')) { + final editableAlarm = _editableTaskDisplayAlarm(component); + final desired = _taskReminder(fields); + if (desired != null || editableAlarm.index != null) { + operations.add( + DavPatchOperation.replaceAlarm( + alarmIndex: editableAlarm.index ?? component.alarms.length, + alarm: desired, + ), + ); + } + } + if (operations.isEmpty) return null; + final timestamp = _utcIcal( + (nowUtc ?? (() => DateTime.now().toUtc()))().toUtc(), + ); + operations + ..add(DavPatchOperation.setRaw('LAST-MODIFIED', timestamp)) + ..add(DavPatchOperation.setRaw('DTSTAMP', timestamp)); + return DavMutationPatch( + target: target, + scope: target.recurrenceIdKey == null + ? DavMutationScope.recurrenceMaster + : DavMutationScope.recurrenceException, + operations: operations, + ); +} + +/// Completes the current instance of a recurring VTODO while retaining an +/// open master for the next instance. +/// +/// This follows the recurring-completion lifecycle used by Nextcloud Tasks: +/// a completed detached instance is recorded with RECURRENCE-ID, the first +/// RRULE advances the master's DUE or DTSTART, the start/due distance is +/// retained, and a COUNT limit is decremented. All components remain in the +/// same RFC 5545 calendar-object resource so the mutation is conditional and +/// atomic from the client's perspective. +DavMutationPatch buildDavRecurringTaskCompletionPatch({ + required IcalComponentKey target, + required String baselineRawIcs, + DateTime? completedAtUtc, + DateTime Function()? nowUtc, +}) { + if (target.componentType.toUpperCase() != 'VTODO' || + target.recurrenceIdKey != null) { + throw _invalidMutation(); + } + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final masters = semantic.components + .where( + (component) => + component.componentType == 'VTODO' && + component.uid == target.uid && + component.recurrenceId == null, + ) + .toList(growable: false); + if (masters.length != 1) throw _invalidMutation(); + final master = masters.single; + if (master.recurrenceRules.isEmpty) throw _invalidMutation(); + final instance = master.due ?? master.start; + if (instance == null) { + return DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setTaskProgress(100, completedAtUtc: completedAtUtc), + ], + ); + } + + final recurrenceId = _nextcloudDetachedTemporal(instance); + if (semantic.components.any( + (component) => + component.componentType == 'VTODO' && + component.uid == target.uid && + component.recurrenceIdKey == recurrenceId.key, + )) { + throw _invalidMutation(); + } + + final now = (nowUtc ?? (() => DateTime.now().toUtc()))().toUtc(); + final timestamp = _utcIcal(now); + final completionTimestamp = _utcIcal((completedAtUtc ?? now).toUtc()); + final detachedDue = master.due == null + ? null + : _nextcloudDetachedTemporal(master.due!, forceDate: instance.isDate); + final detachedStart = master.start == null + ? null + : _nextcloudDetachedTemporal(master.start!, forceDate: instance.isDate); + final exception = IcalComponent( + name: 'VTODO', + children: [ + _property('UID', target.uid), + _property( + 'RECURRENCE-ID', + recurrenceId.raw, + parameters: recurrenceId.parameters, + ), + _property('SUMMARY', encodeIcalText(master.summary ?? '')), + if (_nonEmpty(master.description) case final description?) + _property('DESCRIPTION', encodeIcalText(description)), + if (_nonEmpty(master.location) case final location?) + _property('LOCATION', encodeIcalText(location)), + if (_nonEmpty(master.url) case final url?) _property('URL', url), + if (master.priority case final priority?) + _property('PRIORITY', '$priority'), + _property('CLASS', master.classification ?? 'PUBLIC'), + _property('STATUS', 'COMPLETED'), + _property('PERCENT-COMPLETE', '100'), + _property('COMPLETED', completionTimestamp), + if (detachedDue case final due?) + _property('DUE', due.raw, parameters: due.parameters), + if (detachedStart case final start?) + _property('DTSTART', start.raw, parameters: start.parameters), + _property('CREATED', timestamp), + _property('LAST-MODIFIED', timestamp), + _property('DTSTAMP', timestamp), + ], + originalBeginLine: 'BEGIN:VTODO', + originalEndLine: 'END:VTODO', + structurallyDirty: true, + ); + final operations = [ + DavPatchOperation.addComponent(exception), + ]; + + final firstRule = master.recurrenceRules.first; + final count = _rruleCount(firstRule); + if (count != null && count <= 1) { + operations + ..add( + DavPatchOperation.setTaskProgress( + 100, + completedAtUtc: completedAtUtc ?? now, + ), + ) + ..add(DavPatchOperation.setRaw('LAST-MODIFIED', timestamp)) + ..add(DavPatchOperation.setRaw('DTSTAMP', timestamp)); + return DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: operations, + ); + } + + final iterationStart = _nextcloudRecurrenceIterationTemporal(instance); + final next = IcalRecurrenceExpander().nextTaskOccurrence( + semantic, + current: iterationStart, + recurrenceRule: firstRule, + ); + if (next == null) { + return DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: operations, + ); + } + + final nextMaster = _nextcloudAdvancedTemporal(next, allDay: instance.isDate); + if (master.due != null) { + operations.add( + DavPatchOperation.setRaw( + 'DUE', + nextMaster.value, + parameters: nextMaster.parameters, + ), + ); + final start = master.start; + if (start != null) { + final offset = master.due!.localValue.difference(start.localValue); + final nextStart = _nextcloudAdvancedTemporal( + IcalTemporalValue( + rawValue: '', + kind: next.kind, + localValue: next.localValue.subtract(offset), + timeZoneId: null, + ), + allDay: instance.isDate, + ); + operations.add( + DavPatchOperation.setRaw( + 'DTSTART', + nextStart.value, + parameters: nextStart.parameters, + ), + ); + } + } else { + operations.add( + DavPatchOperation.setRaw( + 'DTSTART', + nextMaster.value, + parameters: nextMaster.parameters, + ), + ); + } + operations + ..add(DavPatchOperation.setRaw('PERCENT-COMPLETE', null)) + ..add(DavPatchOperation.setRaw('COMPLETED', null)) + ..add(DavPatchOperation.setRaw('STATUS', null)); + if (count != null) { + final rules = master.documentComponent + .propertiesNamed('RRULE') + .toList(growable: false); + operations.add( + DavPatchOperation.replaceRepeatedRaw('RRULE', [ + for (var index = 0; index < rules.length; index += 1) + DavRawPropertyValue( + value: index == 0 + ? _replaceRruleCount(rules[index].rawValue, count - 1) + : rules[index].rawValue, + parameters: rules[index].parameters, + ), + ]), + ); + } + operations + ..add(DavPatchOperation.setRaw('LAST-MODIFIED', timestamp)) + ..add(DavPatchOperation.setRaw('DTSTAMP', timestamp)); + return DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: operations, + ); +} + +List reminderMinutes(Object? reminders) { + if (reminders is! Map) return const []; + final map = reminders.cast(); + final values = []; + if (map['reminderMinutesBeforeStart'] is int) { + values.add(map['reminderMinutesBeforeStart']! as int); + } + if (map['overrides'] is List) { + for (final value in map['overrides']! as List) { + if (value is Map && value['minutes'] is int) { + values.add(value['minutes']! as int); + } + } + } + return values.where((value) => value >= 0).toSet().toList()..sort(); +} + +typedef _RawTemporal = ({String value, List parameters}); + +_RawTemporal _eventTemporal( + DateTime value, { + required bool allDay, + required String? timeZone, +}) { + if (allDay) { + return ( + value: _date(value), + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ], + ); + } + return _dateTimeTemporal(value, timeZone); +} + +_RawTemporal _dateTimeTemporal(DateTime value, String? timeZone) { + final zone = timeZone?.trim(); + if (value.isUtc) { + return (value: _utcIcal(value), parameters: const []); + } + if (_isUtcZone(zone)) { + return (value: '${_localIcal(value)}Z', parameters: const []); + } + return ( + value: _localIcal(value), + parameters: zone == null || zone.isEmpty + ? const [] + : [ + IcalParameter(name: 'TZID', values: [zone], wasQuoted: false), + ], + ); +} + +_RawTemporal? _taskTemporal( + Map fields, { + required String prefix, +}) { + final dateTimeKey = 'microsoft${prefix}DateTime'; + final zoneKey = 'microsoft${prefix}TimeZone'; + final dateTimeValue = fields[dateTimeKey]; + final map = dateTimeValue is Map + ? dateTimeValue.cast() + : null; + final text = map?['dateTime']?.toString() ?? dateTimeValue?.toString(); + final zone = fields[zoneKey]?.toString() ?? map?['timeZone']?.toString(); + if (text != null && text.isNotEmpty) { + final parsed = DateTime.tryParse(text); + if (parsed == null) throw _invalidMutation(); + if (!text.contains('T')) { + return ( + value: _date(parsed), + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ], + ); + } + return _dateTimeTemporal(parsed, zone); + } + if (prefix == 'Due') { + final due = fields['due']; + if (due == null) return null; + final parsed = due is DateTime ? due : DateTime.tryParse(due.toString()); + if (parsed == null) throw _invalidMutation(); + return ( + value: _date(parsed), + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ], + ); + } + return null; +} + +bool _containsTaskTemporal( + Map fields, { + required String prefix, +}) => + fields.containsKey('microsoft${prefix}DateTime') || + fields.containsKey('microsoft${prefix}TimeZone') || + (prefix == 'Due' && fields.containsKey('due')); + +List _recurrenceOperations(Object? recurrence) { + final rules = []; + if (recurrence is List) { + rules.addAll(recurrence.map((value) => value.toString())); + } else if (recurrence is Map && recurrence['rules'] is List) { + rules.addAll( + (recurrence['rules']! as List).map((value) => value.toString()), + ); + } + return [ + DavPatchOperation.replaceRepeatedRaw('RRULE', [ + for (final rule in rules) + DavRawPropertyValue(value: rule.replaceFirst(RegExp(r'^RRULE:'), '')), + ]), + ]; +} + +List _taskRecurrenceOperations(Object? recurrence) { + if (recurrence == null) return _recurrenceOperations(null); + if (recurrence is Map && recurrence['pattern'] is Map) { + return _recurrenceOperations([_graphRecurrenceToRrule(recurrence)]); + } + return _recurrenceOperations(recurrence); +} + +String _graphRecurrenceToRrule(Map recurrence) { + final pattern = recurrence['pattern']; + if (pattern is! Map) throw _invalidMutation(); + final type = pattern['type']?.toString(); + final frequency = switch (type) { + 'daily' => 'DAILY', + 'weekly' => 'WEEKLY', + 'absoluteMonthly' => 'MONTHLY', + 'absoluteYearly' => 'YEARLY', + _ => throw _invalidMutation(), + }; + final parts = [ + 'FREQ=$frequency', + 'INTERVAL=${pattern['interval'] is int ? pattern['interval'] : 1}', + ]; + if (type == 'weekly' && pattern['daysOfWeek'] is List) { + final days = [ + for (final value in pattern['daysOfWeek'] as List) + switch (value.toString().toLowerCase()) { + 'monday' => 'MO', + 'tuesday' => 'TU', + 'wednesday' => 'WE', + 'thursday' => 'TH', + 'friday' => 'FR', + 'saturday' => 'SA', + 'sunday' => 'SU', + _ => throw _invalidMutation(), + }, + ]; + if (days.isNotEmpty) parts.add('BYDAY=${days.join(',')}'); + } + if (type == 'absoluteMonthly' && pattern['dayOfMonth'] is int) { + parts.add('BYMONTHDAY=${pattern['dayOfMonth']}'); + } + if (type == 'absoluteYearly') { + if (pattern['month'] is int) parts.add('BYMONTH=${pattern['month']}'); + if (pattern['dayOfMonth'] is int) { + parts.add('BYMONTHDAY=${pattern['dayOfMonth']}'); + } + } + return parts.join(';'); +} + +List _alarmOperations( + List minutes, { + required int startIndex, +}) => [ + for (var index = 0; index < minutes.length; index += 1) + DavPatchOperation.replaceAlarm( + alarmIndex: startIndex + index, + alarm: _displayAlarm(minutes[index]), + ), +]; + +IcalComponent _displayAlarm(int minutes) => IcalComponent( + name: 'VALARM', + children: [ + _property('ACTION', 'DISPLAY'), + _property('DESCRIPTION', 'Reminder'), + _property('TRIGGER', '-PT${minutes}M'), + ], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + structurallyDirty: true, +); + +({int? index, List minutes}) _editableDisplayAlarm( + IcalSemanticComponent component, +) { + for (var index = 0; index < component.alarms.length; index += 1) { + final alarm = component.alarms[index]; + if (alarm.firstProperty('ACTION')?.rawValue.toUpperCase() != 'DISPLAY') { + continue; + } + final trigger = alarm.firstProperty('TRIGGER')?.rawValue.toUpperCase(); + final match = trigger == null + ? null + : RegExp(r'^-PT([0-9]+)M$').firstMatch(trigger); + if (match != null) { + return (index: index, minutes: [int.parse(match.group(1)!)]); + } + } + return (index: null, minutes: const []); +} + +({int? index, List minutes}) _editableTaskDisplayAlarm( + IcalSemanticComponent component, +) { + for (var index = 0; index < component.alarms.length; index += 1) { + final alarm = component.alarms[index]; + if (alarm.firstProperty('ACTION')?.rawValue.toUpperCase() != 'DISPLAY') { + continue; + } + final trigger = alarm.firstProperty('TRIGGER')?.rawValue.toUpperCase(); + if (trigger == null) continue; + try { + if (parseIcalDuration(trigger) != null) { + return (index: index, minutes: const []); + } + } on DavException { + // It may be an absolute trigger. + } + if (RegExp(r'^\d{8}T\d{6}Z$').hasMatch(trigger)) { + return (index: index, minutes: const []); + } + } + return (index: null, minutes: const []); +} + +IcalComponent? _taskReminder(Map fields) { + if (fields['microsoftIsReminderOn'] == false) return null; + final value = fields['microsoftReminderDateTime']; + if (value == null) return null; + final map = value is Map ? value.cast() : null; + final text = map?['dateTime']?.toString() ?? value.toString(); + final parsed = DateTime.tryParse(text); + if (parsed == null) throw _invalidMutation(); + // RFC 5545 absolute TRIGGER values are UTC DATE-TIME values. Inputs without + // an offset are treated as local wall time; conversion through DateTime is + // deterministic for the desktop's current zone and never rewrites an + // untouched imported alarm. + final trigger = _utcIcal(parsed.isUtc ? parsed : parsed.toUtc()); + return IcalComponent( + name: 'VALARM', + children: [ + _property('ACTION', 'DISPLAY'), + _property('DESCRIPTION', 'Reminder'), + _property( + 'TRIGGER', + trigger, + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE-TIME'], wasQuoted: false), + ], + ), + ], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + structurallyDirty: true, + ); +} + +List _taskStateOperations( + Map fields, { + required IcalSemanticComponent current, +}) { + final hasStatus = + fields.containsKey('taskStatus') || fields.containsKey('status'); + final hasPercent = fields.containsKey('percentComplete'); + final hasCompleted = fields.containsKey('completedAtUtc'); + if (!hasStatus && !hasPercent && !hasCompleted) return const []; + + DateTime? completedAt; + if (hasCompleted && fields['completedAtUtc'] != null) { + completedAt = DateTime.tryParse(fields['completedAtUtc'].toString()); + if (completedAt == null) throw _invalidMutation(); + } + if (completedAt != null) { + return [ + DavPatchOperation.setTaskProgress( + 100, + completedAtUtc: completedAt.toUtc(), + ), + ]; + } + if (hasPercent) { + final raw = fields['percentComplete']; + if (raw is! int || raw < 0 || raw > 100) throw _invalidMutation(); + return [DavPatchOperation.setTaskProgress(raw)]; + } + + if (hasStatus) { + final rawStatus = fields.containsKey('taskStatus') + ? fields['taskStatus'] + : fields['status']; + final status = _normalizedTaskStatus(rawStatus); + final percent = current.percentComplete ?? 0; + return switch (status) { + 'COMPLETED' => [DavPatchOperation.setTaskProgress(100)], + 'IN-PROCESS' => [ + DavPatchOperation.setTaskProgress( + percent == 100 + ? 99 + : percent == 0 + ? 1 + : percent, + ), + ], + 'NEEDS-ACTION' || null => [ + DavPatchOperation.setRaw('STATUS', status), + DavPatchOperation.setRaw('COMPLETED', null), + if (percent == 100) DavPatchOperation.setRaw('PERCENT-COMPLETE', '99'), + ], + 'CANCELLED' => [DavPatchOperation.setRaw('STATUS', 'CANCELLED')], + _ => throw _invalidMutation(), + }; + } + + if (hasCompleted) { + if ((current.percentComplete ?? 0) == 100) { + return [DavPatchOperation.setTaskProgress(99)]; + } + return [DavPatchOperation.setRaw('COMPLETED', null)]; + } + return const []; +} + +String? _normalizedTaskStatus(Object? value) { + if (value == null) return null; + return switch (value.toString().trim().toUpperCase()) { + 'COMPLETED' => 'COMPLETED', + 'INPROCESS' || 'IN-PROCESS' => 'IN-PROCESS', + 'NEEDSACTION' || 'NEEDS-ACTION' => 'NEEDS-ACTION', + 'CANCELLED' || 'CANCELED' => 'CANCELLED', + _ => throw _invalidMutation(), + }; +} + +int? _taskPriority(Map fields) { + if (fields.containsKey('icalPriority')) { + final value = fields['icalPriority']; + if (value == null) return null; + if (value is! int || value < 0 || value > 9) throw _invalidMutation(); + return value == 0 ? null : value; + } + return switch (fields['importance']?.toString()) { + 'high' => 1, + 'low' => 9, + 'normal' || null => null, + _ => throw _invalidMutation(), + }; +} + +String? _taskUrl(Object? value) { + final text = value?.toString().trim(); + if (text == null || text.isEmpty) return null; + final uri = Uri.tryParse(text); + if (text.contains('\r') || + text.contains('\n') || + uri == null || + !uri.hasScheme) { + throw _invalidMutation(); + } + return text; +} + +List _taskAlarmOperations( + IcalSemanticComponent component, + Object? value, +) { + if (value is! List) throw _invalidMutation(); + final desired = []; + try { + for (final item in value) { + if (item is! Map) throw _invalidMutation(); + desired.add(IcalTaskAlarm.fromJson(item.cast())); + } + } on DavException { + rethrow; + } on Object { + throw _invalidMutation(); + } + final existing = [ + for (final alarm in component.alarms) IcalTaskAlarm.fromComponent(alarm), + ]; + final operations = []; + final sharedLength = existing.length < desired.length + ? existing.length + : desired.length; + for (var index = 0; index < sharedLength; index += 1) { + if (existing[index] != desired[index]) { + operations.add( + DavPatchOperation.replaceAlarm( + alarmIndex: index, + alarm: desired[index].toComponent(), + ), + ); + } + } + for (var index = existing.length - 1; index >= desired.length; index -= 1) { + operations.add(DavPatchOperation.replaceAlarm(alarmIndex: index)); + } + for (var index = existing.length; index < desired.length; index += 1) { + operations.add( + DavPatchOperation.replaceAlarm( + alarmIndex: index, + alarm: desired[index].toComponent(), + ), + ); + } + return operations; +} + +String? _classification(String? value) => switch (value?.toUpperCase()) { + 'PUBLIC' || 'PRIVATE' || 'CONFIDENTIAL' => value!.toUpperCase(), + 'DEFAULT' || 'NORMAL' || null || '' => null, + _ => null, +}; + +String? _transparency(String? value) => switch (value?.toUpperCase()) { + 'TRANSPARENT' || 'FREE' => 'TRANSPARENT', + 'OPAQUE' || 'BUSY' || 'TENTATIVE' || 'OOF' || 'WORKINGELSEWHERE' => 'OPAQUE', + null || '' => null, + _ => null, +}; + +String _categories(Iterable values) => values + .map((value) => encodeIcalText(value.trim())) + .where((value) => value.isNotEmpty) + .join(','); + +DavPatchOperation _taskCategoriesOperation( + IcalSemanticComponent component, + List desired, +) { + final properties = component.documentComponent + .propertiesNamed('CATEGORIES') + .toList(growable: false); + if (desired.isEmpty) { + return DavPatchOperation.replaceRepeatedRaw('CATEGORIES', const []); + } + if (properties.length <= 1) { + return DavPatchOperation.replaceRepeatedRaw('CATEGORIES', [ + DavRawPropertyValue( + value: _categories(desired), + parameters: properties.firstOrNull?.parameters ?? const [], + ), + ]); + } + + final current = component.categories; + final remove = current.where((value) => !desired.contains(value)).toSet(); + final add = desired.where((value) => !current.contains(value)).toList(); + final retained = <({List values, List parameters})>[]; + for (final property in properties) { + final values = _categoryValues( + property, + ).where((value) => !remove.contains(value)).toList(); + if (values.isNotEmpty) { + retained.add((values: values, parameters: property.parameters)); + } + } + if (retained.isEmpty) { + retained.add((values: [...desired], parameters: const [])); + } else { + retained.first.values.addAll(add); + } + return DavPatchOperation.replaceRepeatedRaw('CATEGORIES', [ + for (final property in retained) + DavRawPropertyValue( + value: _categories(property.values), + parameters: property.parameters, + ), + ]); +} + +List _categoryValues(IcalProperty property) => _splitEscapedValues( + property.rawValue, +).map(decodeIcalText).where((value) => value.isNotEmpty).toList(); + +List _splitEscapedValues(String source) { + final values = []; + var start = 0; + var escaped = false; + for (var index = 0; index < source.length; index += 1) { + final value = source[index]; + if (escaped) { + escaped = false; + } else if (value == r'\') { + escaped = true; + } else if (value == ',') { + values.add(source.substring(start, index)); + start = index + 1; + } + } + values.add(source.substring(start)); + return values; +} + +bool _sameTemporal(IcalTemporalValue? current, _RawTemporal desired) { + if (current == null || current.rawValue != desired.value) return false; + final desiredKind = desired.parameters + .where((parameter) => parameter.name == 'VALUE') + .firstOrNull + ?.values + .firstOrNull; + final desiredZone = desired.parameters + .where((parameter) => parameter.name == 'TZID') + .firstOrNull + ?.values + .firstOrNull; + return (desiredKind == 'DATE') == current.isDate && + desiredZone == current.timeZoneId; +} + +bool _sameIntegers(List left, List right) { + if (left.length != right.length) return false; + for (var index = 0; index < left.length; index += 1) { + if (left[index] != right[index]) return false; + } + return true; +} + +IcalProperty _property( + String name, + String value, { + List parameters = const [], +}) => IcalProperty( + group: null, + name: name, + parameters: parameters, + rawValue: value, + originalPhysicalLines: const [], + isDirty: true, +); + +String? _nonEmpty(String? source) { + final value = source?.trim(); + return value == null || value.isEmpty ? null : source; +} + +bool _isUtcZone(String? zone) => + zone == 'UTC' || zone == 'Etc/UTC' || zone == 'GMT' || zone == 'Etc/GMT'; + +String _date(DateTime value) => + '${value.year.toString().padLeft(4, '0')}' + '${value.month.toString().padLeft(2, '0')}' + '${value.day.toString().padLeft(2, '0')}'; + +String _localIcal(DateTime value) => + '${_date(value)}T${value.hour.toString().padLeft(2, '0')}' + '${value.minute.toString().padLeft(2, '0')}' + '${value.second.toString().padLeft(2, '0')}'; + +String _utcIcal(DateTime value) => '${_localIcal(value.toUtc())}Z'; + +_RecurrenceIdentity _nextcloudDetachedTemporal( + IcalTemporalValue source, { + bool? forceDate, +}) { + final isDate = forceDate ?? source.isDate; + final wall = source.kind == IcalTemporalKind.utcDateTime && !isDate + ? source.localValue.toLocal() + : source.localValue; + final raw = isDate ? _date(wall) : _localIcal(wall); + final parameters = isDate + ? const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ] + : const []; + final property = IcalProperty( + group: null, + name: 'RECURRENCE-ID', + parameters: parameters, + rawValue: raw, + originalPhysicalLines: const [], + isDirty: true, + ); + final key = icalRecurrenceIdKey(property); + if (key == null) throw _invalidMutation(); + return (raw: raw, parameters: parameters, key: key); +} + +IcalTemporalValue _nextcloudRecurrenceIterationTemporal( + IcalTemporalValue source, +) { + final detached = _nextcloudDetachedTemporal(source); + final parsed = parseIcalTemporal( + IcalProperty( + group: null, + name: 'DTSTART', + parameters: detached.parameters, + rawValue: detached.raw, + originalPhysicalLines: const [], + ), + ); + if (parsed == null) throw _invalidMutation(); + return parsed; +} + +_RawTemporal _nextcloudAdvancedTemporal( + IcalTemporalValue source, { + required bool allDay, +}) { + final wall = source.localValue; + if (allDay) { + return ( + value: _date(wall), + parameters: const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ], + ); + } + final local = DateTime( + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ); + return (value: _utcIcal(local.toUtc()), parameters: const []); +} + +int? _rruleCount(String rule) { + for (final segment in rule.split(';')) { + final separator = segment.indexOf('='); + if (separator <= 0) continue; + if (segment.substring(0, separator).toUpperCase() != 'COUNT') continue; + final count = int.tryParse(segment.substring(separator + 1)); + if (count == null || count < 1) throw _invalidMutation(); + return count; + } + return null; +} + +String _replaceRruleCount(String rule, int count) { + if (count < 1) throw _invalidMutation(); + var replaced = false; + final result = [ + for (final segment in rule.split(';')) + if (segment.split('=').first.toUpperCase() == 'COUNT') + (() { + if (replaced) throw _invalidMutation(); + replaced = true; + return '${segment.substring(0, segment.indexOf('='))}=$count'; + })() + else + segment, + ]; + if (!replaced) throw _invalidMutation(); + return result.join(';'); +} + +_RecurrenceIdentity _recurrenceIdentity(String occurrenceKey) { + String raw; + List parameters; + if (occurrenceKey.startsWith('DATE:')) { + raw = occurrenceKey.substring('DATE:'.length); + parameters = const [ + IcalParameter(name: 'VALUE', values: ['DATE'], wasQuoted: false), + ]; + } else if (occurrenceKey.startsWith('FLOATING:')) { + raw = occurrenceKey.substring('FLOATING:'.length); + parameters = const []; + } else if (occurrenceKey.startsWith('UTC:')) { + final instant = DateTime.tryParse(occurrenceKey.substring('UTC:'.length)); + if (instant == null) throw _invalidMutation(); + raw = _utcIcal(instant); + parameters = const []; + } else if (occurrenceKey.startsWith('TZID=')) { + final separator = occurrenceKey.indexOf(':', 'TZID='.length); + if (separator <= 'TZID='.length || separator == occurrenceKey.length - 1) { + throw _invalidMutation(); + } + final timeZone = occurrenceKey.substring('TZID='.length, separator); + raw = occurrenceKey.substring(separator + 1); + parameters = [ + IcalParameter(name: 'TZID', values: [timeZone], wasQuoted: false), + ]; + } else { + throw _invalidMutation(); + } + final property = IcalProperty( + group: null, + name: 'RECURRENCE-ID', + parameters: parameters, + rawValue: raw, + originalPhysicalLines: const [], + isDirty: true, + ); + final key = icalRecurrenceIdKey(property); + if (key == null) throw _invalidMutation(); + return (raw: raw, parameters: parameters, key: key); +} + +typedef _RecurrenceIdentity = ({ + String raw, + List parameters, + String key, +}); + +DavException _invalidMutation() => const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavProjectionMutationInvalid', + safeMessage: 'The calendar or task edit could not be represented safely.', +); diff --git a/lib/src/dav/mutation/dav_task_list_mutation_service.dart b/lib/src/dav/mutation/dav_task_list_mutation_service.dart new file mode 100644 index 0000000..e67faa1 --- /dev/null +++ b/lib/src/dav/mutation/dav_task_list_mutation_service.dart @@ -0,0 +1,575 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; + +import '../../core/secrets/secret_store.dart'; +import '../../db/app_database.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../dav_provider_profile.dart'; +import '../discovery/dav_discovery_models.dart'; +import '../http/dav_http_transport.dart'; +import '../storage/dav_collection_capabilities.dart'; +import '../xml/dav_xml.dart'; + +const nextcloudDefaultTaskListColor = '#0082C9'; + +abstract interface class DavTaskListMutationClient { + Future createTaskList(String title); + + Future renameTaskList(String collectionId, String title); + + Future deleteTaskList(String collectionId); +} + +/// Applies Nextcloud Tasks list mutations to their backing CalDAV calendar +/// collections, then refreshes discovery so local projections remain +/// authoritative to the server. +final class DavTaskListMutationService implements DavTaskListMutationClient { + DavTaskListMutationService({ + required AppDatabase database, + required SecretStore secretStore, + required http.Client httpClient, + required String accountId, + required Future Function() refreshAfterMutation, + DavTransportLimits transportLimits = const DavTransportLimits(), + DavXmlParser xmlParser = const DavXmlParser(), + String Function()? correlationIdFactory, + }) : _database = database, + _secretStore = secretStore, + _httpClient = httpClient, + _accountId = accountId, + _refreshAfterMutation = refreshAfterMutation, + _transportLimits = transportLimits, + _xmlParser = xmlParser, + _correlationIdFactory = correlationIdFactory ?? const Uuid().v4; + + final AppDatabase _database; + final SecretStore _secretStore; + final http.Client _httpClient; + final String _accountId; + final Future Function() _refreshAfterMutation; + final DavTransportLimits _transportLimits; + final DavXmlParser _xmlParser; + final String Function() _correlationIdFactory; + + @override + Future createTaskList(String title) async { + final displayName = _requiredTitle(title); + final context = await _loadContext(requireCalendarHome: true); + final existing = await _activeCollections(); + if (existing.any( + (collection) => + collection.taskProjectionEnabled && + collection.displayName == displayName, + )) { + throw const DavException( + kind: DavErrorKind.conflict, + code: 'DavTaskListNameConflict', + safeMessage: 'A Nextcloud task list already uses this name.', + ); + } + + final homeUri = _collectionUri(context.calendarHomeUri!); + final memberName = nextcloudCollectionMemberName( + displayName, + homeUri: homeUri, + existingCollectionUris: [ + for (final collection in existing) Uri.parse(collection.requestUri), + ], + ); + final targetUri = homeUri.resolve(memberName); + final correlationId = _correlationIdFactory(); + try { + final response = await context.transport.send( + DavRequest.xml( + method: 'MKCOL', + uri: targetUri, + accountId: _accountId, + correlationId: correlationId, + body: _taskCollectionMkcolXml( + displayName: displayName, + color: nextcloudDefaultTaskListColor, + ), + retryClass: DavRetryClass.never, + ), + credential: context.credential, + ); + _requireSuccessfulMutation(response, operation: 'create the task list'); + } on DavException catch (error) { + if (!_mayHaveCommitted(error) || + !await _isMatchingTaskCollection( + context, + targetUri, + displayName: displayName, + )) { + rethrow; + } + } + await _refreshAfterMutation(); + } + + @override + Future renameTaskList(String collectionId, String title) async { + final displayName = _requiredTitle(title); + final context = await _loadContext(); + final collection = await _requiredTaskCollection(collectionId); + final capabilities = collectionCapabilitiesFromStored(collection); + if (!capabilities.canWriteProperties) { + throw _readOnlyError(); + } + final existing = await _activeCollections(); + if (existing.any( + (candidate) => + candidate.id != collection.id && + candidate.taskProjectionEnabled && + candidate.displayName == displayName, + )) { + throw const DavException( + kind: DavErrorKind.conflict, + code: 'DavTaskListNameConflict', + safeMessage: 'A Nextcloud task list already uses this name.', + ); + } + if (collection.displayName == displayName) return; + + final response = await context.transport.send( + DavRequest.xml( + method: 'PROPPATCH', + uri: Uri.parse(collection.requestUri), + accountId: _accountId, + collectionId: collection.id, + correlationId: _correlationIdFactory(), + body: _displayNameProppatchXml(displayName), + retryClass: DavRetryClass.never, + ), + credential: context.credential, + ); + _requireSuccessfulMutation(response, operation: 'rename the task list'); + await _refreshAfterMutation(); + } + + @override + Future deleteTaskList(String collectionId) async { + final context = await _loadContext(); + final collection = await _requiredTaskCollection(collectionId); + final capabilities = collectionCapabilitiesFromStored(collection); + final shared = await _isSharedWithAccount(collection); + // Nextcloud Tasks exposes Delete for writable owned lists and Unshare for + // collections shared with the current account. + if (capabilities.isReadOnly && !shared) { + throw _readOnlyError(); + } + + final response = await context.transport.send( + DavRequest( + method: 'DELETE', + uri: Uri.parse(collection.requestUri), + accountId: _accountId, + collectionId: collection.id, + correlationId: _correlationIdFactory(), + retryClass: DavRetryClass.never, + ), + credential: context.credential, + ); + if (response.statusCode != HttpStatus.notFound) { + _requireSuccessfulMutation(response, operation: 'delete the task list'); + } + await _refreshAfterMutation(); + } + + Future<_DavTaskListContext> _loadContext({ + bool requireCalendarHome = false, + }) async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingleOrNull(); + if (account == null || + BusyProviderCodec.requireStorageValue(account.provider) != + BusyProvider.nextcloud) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavTaskListMutationRequiresNextcloud', + safeMessage: 'Task-list collection changes require Nextcloud.', + ); + } + final authority = Uri.tryParse(account.authority); + if (authority == null) { + throw _invalidContextError(); + } + final secret = await _secretStore.readCredential(_accountId); + if (secret is! NextcloudSecretRecord || + secret.canonicalServer != authority || + secret.loginName != account.providerAccountId) { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavCredentialsRevoked', + safeMessage: 'The Nextcloud credential is unavailable or invalid.', + ); + } + final service = await (_database.select( + _database.davAccountServices, + )..where((row) => row.accountId.equals(_accountId))).getSingleOrNull(); + final calendarHome = Uri.tryParse(service?.calendarHomeHref ?? ''); + if (requireCalendarHome && calendarHome == null) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavCalendarHomeUnavailable', + safeMessage: 'The Nextcloud calendar home is unavailable.', + ); + } + final profile = davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: authority, + ); + return _DavTaskListContext( + transport: DavHttpTransport( + client: _httpClient, + profile: profile, + accountAuthority: authority, + limits: _transportLimits, + ), + credential: DavBasicCredential( + username: secret.loginName, + password: secret.appPassword, + ), + calendarHomeUri: calendarHome, + ); + } + + Future> _activeCollections() { + return (_database.select(_database.davCollections)..where( + (row) => + row.accountId.equals(_accountId) & + row.deleted.equals(false) & + row.serverMissing.equals(false), + )) + .get(); + } + + Future _requiredTaskCollection(String collectionId) async { + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingleOrNull(); + if (collection == null || + collection.accountId != _accountId || + collection.deleted || + collection.serverMissing || + !collection.taskProjectionEnabled || + collection.supportedComponentMask & davComponentTodo == 0) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavTaskListCollectionNotFound', + safeMessage: 'The Nextcloud task list is no longer available.', + ); + } + return collection; + } + + Future _isSharedWithAccount(DavCollection collection) async { + final service = await (_database.select( + _database.davAccountServices, + )..where((row) => row.accountId.equals(_accountId))).getSingleOrNull(); + final owner = _normalizedHrefPath(collection.ownerHref); + final principal = _normalizedHrefPath(service?.principalHref); + return owner != null && principal != null && owner != principal; + } + + Future _isMatchingTaskCollection( + _DavTaskListContext context, + Uri uri, { + required String displayName, + }) async { + try { + final response = await context.transport.send( + DavRequest.xml( + method: 'PROPFIND', + uri: uri, + accountId: _accountId, + correlationId: _correlationIdFactory(), + headers: const {'depth': '0'}, + body: _taskCollectionProbeXml, + retryClass: DavRetryClass.safeRead, + ), + credential: context.credential, + ); + if (response.statusCode == HttpStatus.notFound || + response.statusCode != HttpStatus.multiStatus) { + return false; + } + final multistatus = _xmlParser.parseMultistatus( + response.bodyBytes, + correlationId: response.correlationId, + ); + for (final item in multistatus.responses) { + final resourceType = item.successfulProperty( + davNamespace, + 'resourcetype', + ); + final components = item.successfulProperty( + caldavNamespace, + 'supported-calendar-component-set', + ); + final name = item + .successfulProperty(davNamespace, 'displayname') + ?.text + .trim(); + final isCalendar = resourceType?.childElements.any( + (element) => + element.name.namespaceUri == caldavNamespace && + element.name.local == 'calendar', + ); + final supportsTasks = components?.childElements.any( + (element) => + element.name.namespaceUri == caldavNamespace && + element.name.local == 'comp' && + element.getAttribute('name')?.toUpperCase() == 'VTODO', + ); + if (isCalendar == true && + supportsTasks == true && + name == displayName) { + return true; + } + } + } on Object { + return false; + } + return false; + } + + void _requireSuccessfulMutation( + DavResponse response, { + required String operation, + }) { + if (response.statusCode < 200 || response.statusCode >= 300) { + throw _statusError( + response.statusCode, + operation: operation, + correlationId: response.correlationId, + ); + } + if (response.statusCode != HttpStatus.multiStatus) return; + final multistatus = _xmlParser.parseMultistatus( + response.bodyBytes, + correlationId: response.correlationId, + ); + if (multistatus.responses.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavCollectionMutationEmptyMultistatus', + safeMessage: 'Nextcloud returned an invalid collection response.', + statusCode: response.statusCode, + correlationId: response.correlationId, + ); + } + for (final item in multistatus.responses) { + final responseStatus = item.statusCode; + if (responseStatus != null && + (responseStatus < 200 || responseStatus >= 300)) { + throw _statusError( + responseStatus, + operation: operation, + correlationId: response.correlationId, + ); + } + for (final propstat in item.propstats) { + if (!propstat.isSuccessful) { + throw _statusError( + propstat.statusCode, + operation: operation, + correlationId: response.correlationId, + ); + } + } + } + } +} + +String nextcloudCollectionMemberName( + String displayName, { + required Uri homeUri, + required Iterable existingCollectionUris, +}) { + var candidate = displayName + .toLowerCase() + .replaceAll(RegExp(r'\s+'), '-') + .replaceAll(RegExp(r'[^\w-]+'), '') + .replaceAll(RegExp(r'--+'), '-') + .replaceFirst(RegExp(r'^-+'), '') + .replaceFirst(RegExp(r'-+$'), ''); + if (candidate.isEmpty) candidate = '-'; + final home = _collectionUri(homeUri); + final occupied = { + for (final uri in existingCollectionUris) _collectionUri(uri).toString(), + }; + bool available(String value) => + !occupied.contains(_collectionUri(home.resolve(value)).toString()); + if (available(candidate)) return candidate; + if (!candidate.contains('-')) { + candidate = '$candidate-1'; + if (available(candidate)) return candidate; + } + do { + final lastDash = candidate.lastIndexOf('-'); + final first = candidate.substring(0, lastDash); + final suffix = candidate.substring(lastDash + 1); + final number = int.tryParse(suffix); + candidate = number == null ? '$candidate-1' : '$first-${number + 1}'; + } while (!available(candidate)); + return candidate; +} + +final class _DavTaskListContext { + const _DavTaskListContext({ + required this.transport, + required this.credential, + required this.calendarHomeUri, + }); + + final DavHttpTransport transport; + final DavBasicCredential credential; + final Uri? calendarHomeUri; +} + +Uri _collectionUri(Uri uri) { + final path = uri.path.endsWith('/') ? uri.path : '${uri.path}/'; + return uri.replace(path: path, query: null, fragment: null); +} + +String _requiredTitle(String title) { + final value = title.trim(); + if (value.isEmpty) { + throw ArgumentError.value(title, 'title', 'A task-list name is required.'); + } + return value; +} + +String _xmlText(String value) => value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>'); + +String _taskCollectionMkcolXml({ + required String displayName, + required String color, +}) => + '' + '' + '' + '' + '${_xmlText(displayName)}' + '${_xmlText(color)}' + '1' + '' + '' + '' + '' + ''; + +String _displayNameProppatchXml(String displayName) => + '' + '' + '' + '${_xmlText(displayName)}' + '' + ''; + +const _taskCollectionProbeXml = + '' + '' + '' + '' + ''; + +bool _mayHaveCommitted(DavException error) => switch (error.kind) { + DavErrorKind.timeout || DavErrorKind.network || DavErrorKind.server => true, + _ => false, +}; + +DavException _statusError( + int statusCode, { + required String operation, + required String correlationId, +}) { + final mapped = switch (statusCode) { + HttpStatus.unauthorized => ( + DavErrorKind.authentication, + 'DavAuthRejected', + 'Nextcloud rejected the account credential.', + ), + HttpStatus.forbidden => ( + DavErrorKind.authorization, + 'DavPermissionDenied', + 'Nextcloud did not allow this task-list change.', + ), + HttpStatus.notFound || HttpStatus.gone => ( + DavErrorKind.notFound, + 'DavTaskListCollectionNotFound', + 'The Nextcloud task list is no longer available.', + ), + HttpStatus.conflict || + HttpStatus.preconditionFailed || + HttpStatus.locked || + HttpStatus.methodNotAllowed => ( + DavErrorKind.conflict, + 'DavTaskListCollectionConflict', + 'Nextcloud could not $operation because the collection changed.', + ), + HttpStatus.tooManyRequests => ( + DavErrorKind.rateLimited, + 'DavRateLimited', + 'Nextcloud temporarily limited task-list changes.', + ), + HttpStatus.insufficientStorage => ( + DavErrorKind.limitExceeded, + 'DavQuotaOrSizeLimit', + 'Nextcloud has insufficient storage for this task-list change.', + ), + >= 500 => ( + DavErrorKind.server, + 'DavServerUnavailable', + 'Nextcloud could not complete the task-list change.', + ), + _ => ( + DavErrorKind.protocol, + 'DavTaskListCollectionMutationRejected', + 'Nextcloud rejected the task-list change.', + ), + }; + return DavException( + kind: mapped.$1, + code: mapped.$2, + safeMessage: mapped.$3, + statusCode: statusCode, + correlationId: correlationId, + ); +} + +DavException _readOnlyError() => const DavException( + kind: DavErrorKind.authorization, + code: 'DavCollectionReadOnly', + safeMessage: 'This Nextcloud task list is read-only.', + categoryOverride: DavErrorCategory.davReadOnly, +); + +DavException _invalidContextError() => const DavException( + kind: DavErrorKind.protocol, + code: 'DavTaskListMutationContextInvalid', + safeMessage: 'The Nextcloud task-list connection is invalid.', +); + +String? _normalizedHrefPath(String? value) { + final uri = Uri.tryParse(value ?? ''); + if (uri == null || uri.path.isEmpty) return null; + var path = uri.path; + while (path.length > 1 && path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + return path; +} diff --git a/lib/src/dav/storage/dav_collection_capabilities.dart b/lib/src/dav/storage/dav_collection_capabilities.dart new file mode 100644 index 0000000..d63f79e --- /dev/null +++ b/lib/src/dav/storage/dav_collection_capabilities.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; + +import '../../db/app_database.dart'; +import '../../providers/provider_capabilities.dart'; +import '../dav_errors.dart'; +import '../discovery/dav_discovery_models.dart'; + +/// Rehydrates the effective capability object persisted during discovery. +/// +/// Mutation entry points use this function immediately before queueing or +/// replaying work. That keeps ACL and component-set changes authoritative and +/// prevents presentation code from inferring write access from provider type. +CollectionCapabilities collectionCapabilitiesFromStored( + DavCollection collection, +) { + final privileges = _stringSet(collection.currentUserPrivilegesJson); + final reports = _stringSet(collection.supportedReportsJson); + final aggregateAll = privileges.contains('{DAV:}all'); + final aggregateWrite = aggregateAll || privileges.contains('{DAV:}write'); + return CollectionCapabilities( + canRead: + aggregateAll || + privileges.contains('{DAV:}write') || + privileges.contains('{DAV:}read'), + canReadPrivileges: + privileges.contains('{DAV:}read-current-user-privilege-set') || + aggregateAll, + canWriteContent: + !collection.readOnly && + (aggregateWrite || privileges.contains('{DAV:}write-content')), + canWriteProperties: + !collection.readOnly && + (aggregateWrite || privileges.contains('{DAV:}write-properties')), + canAddMembers: + !collection.readOnly && + (aggregateWrite || privileges.contains('{DAV:}bind')), + canDeleteMembers: + !collection.readOnly && + (aggregateWrite || privileges.contains('{DAV:}unbind')), + canReadFreeBusy: + privileges.contains('{urn:ietf:params:xml:ns:caldav}read-free-busy') || + aggregateAll, + supportsEvents: collection.supportedComponentMask & davComponentEvent != 0, + supportsTasks: collection.supportedComponentMask & davComponentTodo != 0, + supportsSyncCollection: reports.contains('{DAV:}sync-collection'), + supportsCalendarMultiget: reports.contains( + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ), + supportsCalendarQuery: reports.contains( + '{urn:ietf:params:xml:ns:caldav}calendar-query', + ), + supportedCalendarData: _calendarData(collection.supportedCalendarDataJson), + maximumResourceSize: collection.maximumResourceSize, + maximumInstances: collection.maximumInstances, + ); +} + +Set _stringSet(String source) { + try { + final decoded = jsonDecode(source); + if (decoded is! List || decoded.any((value) => value is! String)) { + throw _invalidCapabilities(); + } + return decoded.cast().toSet(); + } on DavException { + rethrow; + } on Object { + throw _invalidCapabilities(); + } +} + +Set _calendarData(String source) { + try { + final decoded = jsonDecode(source); + if (decoded is! List) throw _invalidCapabilities(); + return { + for (final value in decoded) + if (value is String) + value + else if (value is Map && value['contentType'] is String) + value['contentType']! as String + else + throw _invalidCapabilities(), + }; + } on DavException { + rethrow; + } on Object { + throw _invalidCapabilities(); + } +} + +DavException _invalidCapabilities() => const DavException( + kind: DavErrorKind.protocol, + code: 'DavStoredCapabilitiesInvalid', + safeMessage: 'Stored DAV collection capabilities were invalid.', +); diff --git a/lib/src/dav/storage/dav_object_repository.dart b/lib/src/dav/storage/dav_object_repository.dart new file mode 100644 index 0000000..5d0ee3e --- /dev/null +++ b/lib/src/dav/storage/dav_object_repository.dart @@ -0,0 +1,1467 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:drift/drift.dart'; +import 'package:uuid/uuid.dart'; + +import '../../db/app_database.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../ical/ical_document.dart'; +import '../ical/ical_recurrence.dart'; +import '../ical/ical_semantics.dart'; + +const davRawObjectParserVersion = 1; +const davProjectionVersion = 1; +const davSyncStateSchemaVersion = 1; + +final class DavPreparedObject { + const DavPreparedObject._({ + required this.hrefKey, + required this.requestUri, + required this.etag, + required this.contentType, + required this.rawIcsBody, + required this.rawBodyHash, + required this.semantic, + }); + + factory DavPreparedObject.parse({ + required String hrefKey, + required Uri requestUri, + required String? etag, + required String? contentType, + required String rawIcsBody, + int maximumResourceBytes = 16 * 1024 * 1024, + }) { + final trimmedHref = hrefKey.trim(); + if (trimmedHref.isEmpty || + !trimmedHref.startsWith('/') || + requestUri.userInfo.isNotEmpty || + requestUri.hasFragment || + requestUri.hasQuery) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavInvalidObjectIdentity', + safeMessage: 'A DAV object had an invalid resource identity.', + ); + } + final bytes = utf8.encode(rawIcsBody); + if (bytes.length > maximumResourceBytes) { + throw const DavException( + kind: DavErrorKind.maximumResourceSize, + code: 'DavCalendarObjectTooLarge', + safeMessage: 'A calendar object exceeded the configured size limit.', + ); + } + return DavPreparedObject._( + hrefKey: trimmedHref, + requestUri: requestUri, + etag: etag, + contentType: contentType, + rawIcsBody: rawIcsBody, + rawBodyHash: sha256.convert(bytes).toString(), + semantic: IcalSemanticDocument.parse(rawIcsBody), + ); + } + + final String hrefKey; + final Uri requestUri; + final String? etag; + final String? contentType; + final String rawIcsBody; + final String rawBodyHash; + final IcalSemanticDocument semantic; +} + +final class DavCollectionCommit { + DavCollectionCommit({ + required this.accountId, + required this.collectionId, + required this.provider, + required this.objects, + required Set deletedHrefKeys, + required this.completeMembership, + required Set membershipHrefKeys, + required this.finalCursorKind, + required this.finalCursorValue, + required this.baselineGeneration, + required this.completedAtUtc, + required this.projectionRangeStartUtc, + required this.projectionRangeEndUtc, + this.forceReprojection = false, + }) : deletedHrefKeys = Set.unmodifiable(deletedHrefKeys), + membershipHrefKeys = Set.unmodifiable(membershipHrefKeys) { + final objectKeys = objects.map((object) => object.hrefKey).toList(); + if (objectKeys.toSet().length != objectKeys.length || + objectKeys.any(this.deletedHrefKeys.contains) || + (completeMembership && + objectKeys.any((key) => !this.membershipHrefKeys.contains(key)))) { + throw ArgumentError('The DAV collection commit is internally invalid.'); + } + if (finalCursorValue.isEmpty || baselineGeneration < 0) { + throw ArgumentError('The DAV cursor state is invalid.'); + } + } + + final String accountId; + final String collectionId; + final BusyProvider provider; + final List objects; + final Set deletedHrefKeys; + final bool completeMembership; + final Set membershipHrefKeys; + final String finalCursorKind; + final String finalCursorValue; + final int baselineGeneration; + final DateTime completedAtUtc; + final DateTime projectionRangeStartUtc; + final DateTime projectionRangeEndUtc; + final bool forceReprojection; +} + +final class DavObjectRepository { + DavObjectRepository({ + required AppDatabase database, + String Function()? idFactory, + IcalRecurrenceExpander? recurrenceExpander, + }) : _database = database, + _idFactory = idFactory ?? const Uuid().v4, + _recurrenceExpander = recurrenceExpander ?? IcalRecurrenceExpander(); + + final AppDatabase _database; + final String Function() _idFactory; + final IcalRecurrenceExpander _recurrenceExpander; + + Future objectByHref(String collectionId, String hrefKey) { + return (_database.select(_database.davObjects)..where( + (row) => + row.collectionId.equals(collectionId) & + row.hrefKey.equals(hrefKey), + )) + .getSingleOrNull(); + } + + Future> liveObjects(String collectionId) { + return (_database.select(_database.davObjects)..where( + (row) => + row.collectionId.equals(collectionId) & + row.serverDeleted.equals(false), + )) + .get(); + } + + Future cursor(String collectionId) { + return (_database.select(_database.syncCursors)..where( + (row) => + row.davCollectionId.equals(collectionId) & + row.transport.equals('caldav') & + row.syncScopeKind.equals('collection'), + )) + .getSingleOrNull(); + } + + Future nextBaselineGeneration(String collectionId) async { + final current = await cursor(collectionId); + return (current?.baselineGeneration ?? 0) + 1; + } + + Future markSyncStarted({ + required String accountId, + required String collectionId, + required BusyProvider provider, + required int generation, + }) async { + final existing = await cursor(collectionId); + await _database + .into(_database.syncCursors) + .insertOnConflictUpdate( + SyncCursorsCompanion.insert( + id: existing?.id ?? 'dav-sync-$collectionId', + accountId: accountId, + provider: provider.storageValue, + transport: 'caldav', + syncScopeKind: 'collection', + davCollectionId: Value(collectionId), + cursorKind: existing?.cursorKind ?? 'snapshot_generation', + cursorValue: existing?.cursorValue ?? '0', + baselineGeneration: Value(existing?.baselineGeneration ?? 0), + inProgressCursor: const Value(null), + inProgressGeneration: Value(generation), + lastCompleteSyncAt: Value(existing?.lastCompleteSyncAt), + lastFailureCode: const Value(null), + stateSchemaVersion: const Value(davSyncStateSchemaVersion), + ), + ); + } + + Future markSyncFailed({ + required String collectionId, + required String errorCode, + }) async { + await (_database.update(_database.syncCursors)..where( + (row) => + row.davCollectionId.equals(collectionId) & + row.transport.equals('caldav'), + )) + .write( + SyncCursorsCompanion( + inProgressCursor: const Value(null), + inProgressGeneration: const Value(null), + lastFailureCode: Value(errorCode), + ), + ); + } + + /// Rebuilds the bounded occurrence/task projections entirely from the raw + /// local baseline. Advancing the UI horizon never requires a server-wide + /// download and does not alter the durable transport cursor. + Future> reprojectCollectionFromStored({ + required String accountId, + required String collectionId, + required BusyProvider provider, + required DateTime projectionRangeStartUtc, + required DateTime projectionRangeEndUtc, + DateTime? completedAtUtc, + }) async { + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingle(); + if (collection.accountId != accountId) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavCollectionAccountMismatch', + safeMessage: 'The DAV collection did not belong to the account.', + ); + } + final objects = await liveObjects(collectionId); + final parsed = {}; + for (final object in objects) { + parsed[object.id] = IcalSemanticDocument.parse(object.rawIcsBody); + } + final cursorState = await cursor(collectionId); + final now = (completedAtUtc ?? DateTime.now()).toUtc(); + return _database.transaction(() async { + final affected = {}; + for (final object in objects) { + if (await _hasActivePendingOperation(object.id)) continue; + final componentIds = await _replaceComponentIndex( + object.id, + parsed[object.id]!, + ); + await _replaceProjections( + commit: DavCollectionCommit( + accountId: accountId, + collectionId: collectionId, + provider: provider, + objects: const [], + deletedHrefKeys: const {}, + completeMembership: false, + membershipHrefKeys: const {}, + finalCursorKind: cursorState?.cursorKind ?? 'snapshot_generation', + finalCursorValue: cursorState?.cursorValue ?? '0', + baselineGeneration: cursorState?.baselineGeneration ?? 0, + completedAtUtc: now, + projectionRangeStartUtc: projectionRangeStartUtc, + projectionRangeEndUtc: projectionRangeEndUtc, + ), + collection: collection, + objectId: object.id, + etag: object.etag, + semantic: parsed[object.id]!, + componentIds: componentIds, + ); + affected.add(object.id); + } + await _resolveProjectedTaskParents(collectionId); + if (cursorState != null) { + await (_database.update( + _database.syncCursors, + )..where((row) => row.id.equals(cursorState.id))).write( + SyncCursorsCompanion( + stateJson: Value( + jsonEncode({ + 'projectionRangeStartUtc': projectionRangeStartUtc + .toUtc() + .toIso8601String(), + 'projectionRangeEndUtc': projectionRangeEndUtc + .toUtc() + .toIso8601String(), + 'projectionVersion': davProjectionVersion, + }), + ), + ), + ); + } + await (_database.update( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).write( + DavCollectionsCompanion( + projectionVersion: const Value(davProjectionVersion), + updatedAtUtc: Value(now.toIso8601String()), + ), + ); + return affected; + }); + } + + /// Rebuilds projections from a validated pending mutation candidate without + /// replacing the server-confirmed raw body or ETag. The pending operation + /// remains the durable local overlay and synchronization conflict baseline. + Future> projectLocalMutationCandidate({ + required String accountId, + required String collectionId, + required BusyProvider provider, + required String objectId, + required String candidateRawIcs, + DateTime? projectedAtUtc, + }) async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(accountId))).getSingleOrNull(); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingleOrNull(); + final object = await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (account == null || + collection == null || + object == null || + account.provider != provider.storageValue || + collection.accountId != accountId || + object.accountId != accountId || + object.collectionId != collectionId || + object.serverDeleted) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavLocalCandidateContextInvalid', + safeMessage: 'The pending DAV projection context was invalid.', + ); + } + final semantic = IcalSemanticDocument.parse(candidateRawIcs); + final now = (projectedAtUtc ?? DateTime.now()).toUtc(); + final cursorState = await cursor(collectionId); + final projectionRange = _projectionRange(cursorState, now); + final context = DavCollectionCommit( + accountId: accountId, + collectionId: collectionId, + provider: provider, + objects: const [], + deletedHrefKeys: const {}, + completeMembership: false, + membershipHrefKeys: const {}, + finalCursorKind: cursorState?.cursorKind ?? 'snapshot_generation', + finalCursorValue: cursorState?.cursorValue ?? '0', + baselineGeneration: object.baselineGeneration, + completedAtUtc: now, + projectionRangeStartUtc: projectionRange.start, + projectionRangeEndUtc: projectionRange.end, + ); + return _database.transaction(() async { + final componentIds = await _replaceComponentIndex(objectId, semantic); + await _replaceProjections( + commit: context, + collection: collection, + objectId: objectId, + etag: object.etag, + semantic: semantic, + componentIds: componentIds, + ); + await (_database.update( + _database.calendarEvents, + )..where((row) => row.davObjectId.equals(objectId))).write( + CalendarEventsCompanion( + syncStatus: const Value('pending'), + updatedAtLocal: Value(now.millisecondsSinceEpoch), + ), + ); + await (_database.update( + _database.tasks, + )..where((row) => row.davObjectId.equals(objectId))).write( + TasksCompanion( + localDirty: const Value(true), + updatedLocalAtUtc: Value(now.toIso8601String()), + ), + ); + await _resolveProjectedTaskParents(collectionId); + return {objectId}; + }); + } + + /// Stores the server-confirmed representation from a conditional mutation + /// without advancing the collection sync cursor. A follow-up incremental + /// sync remains responsible for obtaining the provider's next opaque token. + Future> commitConfirmedMutation({ + required String accountId, + required String collectionId, + required BusyProvider provider, + DavPreparedObject? canonicalObject, + String? deletedHrefKey, + DateTime? completedAtUtc, + }) async { + if ((canonicalObject == null) == (deletedHrefKey == null)) { + throw ArgumentError( + 'A confirmed mutation must contain exactly one object outcome.', + ); + } + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingle(); + if (collection.accountId != accountId) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavCollectionAccountMismatch', + safeMessage: 'The DAV collection did not belong to the account.', + ); + } + final cursorState = await cursor(collectionId); + final now = (completedAtUtc ?? DateTime.now()).toUtc(); + final projectionRange = _projectionRange(cursorState, now); + final context = DavCollectionCommit( + accountId: accountId, + collectionId: collectionId, + provider: provider, + objects: const [], + deletedHrefKeys: const {}, + completeMembership: false, + membershipHrefKeys: const {}, + finalCursorKind: cursorState?.cursorKind ?? 'snapshot_generation', + finalCursorValue: cursorState?.cursorValue ?? '0', + baselineGeneration: cursorState?.baselineGeneration ?? 0, + completedAtUtc: now, + projectionRangeStartUtc: projectionRange.start, + projectionRangeEndUtc: projectionRange.end, + ); + return _database.transaction(() async { + if (canonicalObject != null) { + final id = await _upsertPreparedObject( + commit: context, + collection: collection, + prepared: canonicalObject, + ignorePendingOperations: true, + ); + await _resolveProjectedTaskParents(collectionId); + return {id}; + } + final object = await objectByHref(collectionId, deletedHrefKey!); + if (object == null) return const {}; + await _markObjectDeleted(object, context, ignorePendingOperations: true); + await _resolveProjectedTaskParents(collectionId); + return {object.id}; + }); + } + + /// Atomically replaces a confirmed source resource with the canonical + /// resource returned from its destination collection after WebDAV MOVE. + Future> commitConfirmedMove({ + required String accountId, + required String sourceCollectionId, + required String destinationCollectionId, + required BusyProvider provider, + required String sourceHrefKey, + required DavPreparedObject canonicalDestinationObject, + DateTime? completedAtUtc, + }) async { + if (sourceCollectionId == destinationCollectionId) { + throw ArgumentError('A DAV move requires two different collections.'); + } + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(accountId))).getSingleOrNull(); + final collections = + await (_database.select(_database.davCollections)..where( + (row) => + row.id.isIn([sourceCollectionId, destinationCollectionId]), + )) + .get(); + final source = collections + .where((collection) => collection.id == sourceCollectionId) + .firstOrNull; + final destination = collections + .where((collection) => collection.id == destinationCollectionId) + .firstOrNull; + if (account == null || + account.provider != provider.storageValue || + source == null || + destination == null || + source.accountId != accountId || + destination.accountId != accountId || + !_hrefIsMemberOf( + canonicalDestinationObject.hrefKey, + destination.hrefKey, + )) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavMoveContextInvalid', + safeMessage: 'The confirmed DAV move context was invalid.', + ); + } + final now = (completedAtUtc ?? DateTime.now()).toUtc(); + final sourceCursor = await cursor(sourceCollectionId); + final destinationCursor = await cursor(destinationCollectionId); + final sourceRange = _projectionRange(sourceCursor, now); + final destinationRange = _projectionRange(destinationCursor, now); + final sourceContext = DavCollectionCommit( + accountId: accountId, + collectionId: sourceCollectionId, + provider: provider, + objects: const [], + deletedHrefKeys: const {}, + completeMembership: false, + membershipHrefKeys: const {}, + finalCursorKind: sourceCursor?.cursorKind ?? 'snapshot_generation', + finalCursorValue: sourceCursor?.cursorValue ?? '0', + baselineGeneration: sourceCursor?.baselineGeneration ?? 0, + completedAtUtc: now, + projectionRangeStartUtc: sourceRange.start, + projectionRangeEndUtc: sourceRange.end, + ); + final destinationContext = DavCollectionCommit( + accountId: accountId, + collectionId: destinationCollectionId, + provider: provider, + objects: const [], + deletedHrefKeys: const {}, + completeMembership: false, + membershipHrefKeys: const {}, + finalCursorKind: destinationCursor?.cursorKind ?? 'snapshot_generation', + finalCursorValue: destinationCursor?.cursorValue ?? '0', + baselineGeneration: destinationCursor?.baselineGeneration ?? 0, + completedAtUtc: now, + projectionRangeStartUtc: destinationRange.start, + projectionRangeEndUtc: destinationRange.end, + ); + return _database.transaction(() async { + final affected = {}; + final sourceObject = await objectByHref( + sourceCollectionId, + sourceHrefKey, + ); + if (sourceObject != null) { + await _markObjectDeleted( + sourceObject, + sourceContext, + ignorePendingOperations: true, + ); + affected.add(sourceObject.id); + } + final destinationObjectId = await _upsertPreparedObject( + commit: destinationContext, + collection: destination, + prepared: canonicalDestinationObject, + ignorePendingOperations: true, + ); + affected.add(destinationObjectId); + await _resolveProjectedTaskParents(sourceCollectionId); + await _resolveProjectedTaskParents(destinationCollectionId); + return affected; + }); + } + + /// Atomically promotes a fully fetched and parsed collection change set. + /// Network work and parsing happen before this method is entered, so any + /// database/projection error rolls back to the prior complete baseline. + Future> commit(DavCollectionCommit commit) { + return _database.transaction(() async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(commit.accountId))).getSingleOrNull(); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(commit.collectionId))).getSingleOrNull(); + if (account == null || + collection == null || + collection.accountId != commit.accountId || + account.provider != commit.provider.storageValue) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavCollectionAccountMismatch', + safeMessage: 'The DAV collection did not belong to the account.', + ); + } + + final changedObjectIds = {}; + for (final prepared in commit.objects) { + final objectId = await _upsertPreparedObject( + commit: commit, + collection: collection, + prepared: prepared, + ); + changedObjectIds.add(objectId); + } + + final deletedObjectIds = {}; + final explicitDeleted = + await (_database.select(_database.davObjects)..where( + (row) => + row.collectionId.equals(commit.collectionId) & + row.hrefKey.isIn(commit.deletedHrefKeys), + )) + .get(); + for (final object in explicitDeleted) { + await _markObjectDeleted(object, commit); + deletedObjectIds.add(object.id); + } + + if (commit.completeMembership) { + final allObjects = await (_database.select( + _database.davObjects, + )..where((row) => row.collectionId.equals(commit.collectionId))).get(); + for (final object in allObjects) { + if (commit.membershipHrefKeys.contains(object.hrefKey)) { + await (_database.update( + _database.davObjects, + )..where((row) => row.id.equals(object.id))).write( + DavObjectsCompanion( + baselineGeneration: Value(commit.baselineGeneration), + ), + ); + } else if (!object.serverDeleted) { + await _markObjectDeleted(object, commit); + deletedObjectIds.add(object.id); + } + } + } + + if (commit.forceReprojection) { + final live = + await (_database.select(_database.davObjects)..where( + (row) => + row.collectionId.equals(commit.collectionId) & + row.serverDeleted.equals(false), + )) + .get(); + for (final object in live) { + if (changedObjectIds.contains(object.id) || + await _hasActivePendingOperation(object.id)) { + continue; + } + final semantic = IcalSemanticDocument.parse(object.rawIcsBody); + final componentIds = await _replaceComponentIndex( + object.id, + semantic, + ); + await _replaceProjections( + commit: commit, + collection: collection, + objectId: object.id, + etag: object.etag, + semantic: semantic, + componentIds: componentIds, + ); + changedObjectIds.add(object.id); + } + } + + await _resolveProjectedTaskParents(commit.collectionId); + + final completed = commit.completedAtUtc.toUtc(); + final cursorId = 'dav-sync-${commit.collectionId}'; + await _database + .into(_database.syncCursors) + .insertOnConflictUpdate( + SyncCursorsCompanion.insert( + id: cursorId, + accountId: commit.accountId, + provider: commit.provider.storageValue, + transport: 'caldav', + syncScopeKind: 'collection', + davCollectionId: Value(commit.collectionId), + cursorKind: commit.finalCursorKind, + cursorValue: commit.finalCursorValue, + baselineGeneration: Value(commit.baselineGeneration), + inProgressCursor: const Value(null), + inProgressGeneration: const Value(null), + lastCompleteSyncAt: Value(completed.millisecondsSinceEpoch), + lastFailureCode: const Value(null), + stateSchemaVersion: const Value(davSyncStateSchemaVersion), + stateJson: Value( + jsonEncode({ + 'projectionRangeStartUtc': commit.projectionRangeStartUtc + .toUtc() + .toIso8601String(), + 'projectionRangeEndUtc': commit.projectionRangeEndUtc + .toUtc() + .toIso8601String(), + 'projectionVersion': davProjectionVersion, + }), + ), + ), + ); + await (_database.update( + _database.davCollections, + )..where((row) => row.id.equals(commit.collectionId))).write( + DavCollectionsCompanion( + syncToken: Value( + commit.finalCursorKind == 'dav_sync_token' + ? commit.finalCursorValue + : null, + ), + serverMissing: const Value(false), + lastSyncAtUtc: Value(completed.toIso8601String()), + parserVersion: const Value(davRawObjectParserVersion), + projectionVersion: const Value(davProjectionVersion), + updatedAtUtc: Value(completed.toIso8601String()), + ), + ); + await (_database.update( + _database.accounts, + )..where((row) => row.id.equals(commit.accountId))).write( + AccountsCompanion( + lastSuccessfulSyncAtUtc: Value(completed.toIso8601String()), + updatedAtUtc: Value(completed.toIso8601String()), + ), + ); + return {...changedObjectIds, ...deletedObjectIds}; + }); + } + + Future _upsertPreparedObject({ + required DavCollectionCommit commit, + required DavCollection collection, + required DavPreparedObject prepared, + bool ignorePendingOperations = false, + }) async { + final existing = + await (_database.select(_database.davObjects)..where( + (row) => + row.collectionId.equals(commit.collectionId) & + row.hrefKey.equals(prepared.hrefKey), + )) + .getSingleOrNull(); + final objectId = existing?.id ?? _idFactory(); + final now = commit.completedAtUtc.toUtc().toIso8601String(); + final rawChanged = existing?.rawBodyHash != prepared.rawBodyHash; + final semanticChanged = + existing?.semanticHash != prepared.semantic.semanticHash; + await _database + .into(_database.davObjects) + .insertOnConflictUpdate( + DavObjectsCompanion.insert( + id: objectId, + accountId: commit.accountId, + collectionId: commit.collectionId, + hrefKey: prepared.hrefKey, + requestUri: prepared.requestUri.toString(), + etag: Value(prepared.etag), + contentType: Value(prepared.contentType), + dominantComponentType: Value( + prepared.semantic.dominantComponentType, + ), + componentMask: Value(prepared.semantic.componentMask), + primaryUid: Value(prepared.semantic.primaryUid), + rawIcsBody: prepared.rawIcsBody, + rawBodyHash: prepared.rawBodyHash, + semanticHash: Value(prepared.semantic.semanticHash), + serverDeleted: const Value(false), + baselineGeneration: Value(commit.baselineGeneration), + firstSeenAtUtc: existing?.firstSeenAtUtc ?? now, + lastFetchedAtUtc: now, + lastChangedAtUtc: rawChanged + ? now + : existing?.lastChangedAtUtc ?? now, + lastParseStatus: const Value('parsed'), + lastParseErrorCode: const Value(null), + parserVersion: const Value(davRawObjectParserVersion), + ), + ); + + if (rawChanged || + semanticChanged || + existing?.parserVersion != davRawObjectParserVersion || + existing?.serverDeleted == true || + commit.forceReprojection) { + final componentIds = await _replaceComponentIndex( + objectId, + prepared.semantic, + ); + if (ignorePendingOperations || + !await _hasActivePendingOperation(objectId)) { + await _replaceProjections( + commit: commit, + collection: collection, + objectId: objectId, + etag: prepared.etag, + semantic: prepared.semantic, + componentIds: componentIds, + ); + } + } + return objectId; + } + + Future> _replaceComponentIndex( + String objectId, + IcalSemanticDocument semantic, + ) async { + final existing = await (_database.select( + _database.davObjectComponents, + )..where((row) => row.davObjectId.equals(objectId))).get(); + final existingIds = { + for (final row in existing) + _componentKey(row.componentType, row.uid, row.recurrenceIdKey): row.id, + }; + final returnedIds = {}; + final result = {}; + for (final entry in semantic.buildIndex()) { + final key = _componentKey( + entry.componentType, + entry.uid, + entry.recurrenceIdKey, + ); + final id = existingIds[key] ?? _idFactory(); + returnedIds.add(id); + result[key] = id; + await _database + .into(_database.davObjectComponents) + .insertOnConflictUpdate( + DavObjectComponentsCompanion.insert( + id: id, + davObjectId: objectId, + componentType: entry.componentType, + uid: entry.uid, + recurrenceIdKey: Value(entry.recurrenceIdKey), + sequence: Value(entry.sequence), + dtstampUtc: Value(entry.dtstampUtc), + lastModifiedUtc: Value(entry.lastModifiedUtc), + semanticHash: entry.semanticHash, + parserProfileVersion: Value(entry.parserProfileVersion), + ), + ); + } + await (_database.delete(_database.davObjectComponents)..where( + (row) => + row.davObjectId.equals(objectId) & row.id.isNotIn(returnedIds), + )) + .go(); + return result; + } + + Future _replaceProjections({ + required DavCollectionCommit commit, + required DavCollection collection, + required String objectId, + required String? etag, + required IcalSemanticDocument semantic, + required Map componentIds, + }) async { + await _deleteProjections(objectId); + if (semantic.components.isEmpty) return; + final componentType = semantic.components.first.componentType; + if (componentType == 'VEVENT') { + if (!collection.eventProjectionEnabled) return; + await _projectEvents( + commit: commit, + collection: collection, + objectId: objectId, + etag: etag, + semantic: semantic, + componentIds: componentIds, + ); + } else if (componentType == 'VTODO') { + if (!collection.taskProjectionEnabled) return; + await _projectTasks( + commit: commit, + collection: collection, + objectId: objectId, + etag: etag, + semantic: semantic, + componentIds: componentIds, + ); + } + } + + Future _projectEvents({ + required DavCollectionCommit commit, + required DavCollection collection, + required String objectId, + required String? etag, + required IcalSemanticDocument semantic, + required Map componentIds, + }) async { + final sourceId = 'dav-calendar-${commit.collectionId}'; + final source = await (_database.select( + _database.calendarSources, + )..where((row) => row.id.equals(sourceId))).getSingleOrNull(); + if (source == null) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavCalendarProjectionSourceMissing', + safeMessage: 'The calendar projection source was missing.', + ); + } + final occurrences = _recurrenceExpander.expand( + semantic, + rangeStartUtc: commit.projectionRangeStartUtc, + rangeEndUtc: commit.projectionRangeEndUtc, + ); + final recurringUids = { + for (final component in semantic.components) + if (component.componentType == 'VEVENT' && + (component.recurrenceId != null || + component.recurrenceRules.isNotEmpty || + component.recurrenceDates.isNotEmpty || + component.exceptionDates.isNotEmpty)) + component.uid, + }; + final now = commit.completedAtUtc.toUtc().millisecondsSinceEpoch; + for (final occurrence in occurrences) { + final component = occurrence.effectiveComponent; + final componentId = + componentIds[_componentKey( + component.componentType, + component.uid!, + component.recurrenceIdKey, + )]!; + final eventId = _stableProjectionId( + 'dav-event', + '$objectId\u0000${occurrence.occurrenceKey}', + ); + final allDay = occurrence.start.kind == IcalTemporalKind.date; + final recurrence = { + 'rules': occurrence.master.recurrenceRules, + 'dates': occurrence.master.recurrenceDates, + 'excludedDates': occurrence.master.exceptionDates, + }; + final attendees = component.attendees.isEmpty + ? occurrence.master.attendees + : component.attendees; + final organizers = component.organizers.isEmpty + ? occurrence.master.organizers + : component.organizers; + final categories = component.categories.isEmpty + ? occurrence.master.categories + : component.categories; + final alarms = component.alarms.isEmpty + ? occurrence.master.alarms + : component.alarms; + final projectionJson = jsonEncode({ + 'transport': 'caldav', + 'uid': component.uid, + 'occurrenceKey': occurrence.occurrenceKey, + 'nativeStart': _temporalJson(occurrence.start), + if (occurrence.end != null) 'nativeEnd': _temporalJson(occurrence.end!), + 'extensionProperties': { + ...occurrence.master.extensionProperties, + ...component.extensionProperties, + }, + }); + await _database + .into(_database.calendarEvents) + .insertOnConflictUpdate( + CalendarEventsCompanion.insert( + id: eventId, + accountId: commit.accountId, + calendarSourceId: sourceId, + provider: commit.provider.storageValue, + providerCalendarId: collection.hrefKey, + providerEventId: objectId, + davCollectionId: Value(commit.collectionId), + davObjectId: Value(objectId), + davComponentId: Value(componentId), + icalUid: Value(component.uid), + recurrenceIdKey: Value(component.recurrenceIdKey), + occurrenceKey: Value(occurrence.occurrenceKey), + projectionVersion: const Value(davProjectionVersion), + providerRecurringEventId: Value( + recurringUids.contains(occurrence.master.uid) + ? occurrence.master.uid + : null, + ), + providerOriginalStartKey: Value( + recurringUids.contains(occurrence.master.uid) + ? occurrence.occurrenceKey + : null, + ), + etagOrChangeKey: Value(etag), + status: Value(component.status ?? occurrence.master.status), + title: occurrence.summary ?? '', + description: Value(occurrence.description), + location: Value(occurrence.location), + allDay: Value(allDay), + startDate: Value( + allDay ? _storageTemporal(occurrence.start) : null, + ), + startDateTime: Value( + allDay ? null : _storageTemporal(occurrence.start), + ), + startTimeZone: Value(_timeZoneName(occurrence.start)), + endDate: Value( + allDay && occurrence.end != null + ? _storageTemporal(occurrence.end!) + : null, + ), + endDateTime: Value( + !allDay && occurrence.end != null + ? _storageTemporal(occurrence.end!) + : null, + ), + endTimeZone: Value( + occurrence.end == null ? null : _timeZoneName(occurrence.end!), + ), + recurrenceJson: Value(jsonEncode(recurrence)), + remindersJson: Value( + jsonEncode({ + 'minutes': _eventReminderMinutes(alarms), + 'alarms': _alarmProjection(alarms), + }), + ), + attendeesJson: Value(jsonEncode(attendees)), + categoriesJson: Value(jsonEncode(categories)), + organizerJson: Value( + organizers.isEmpty ? null : jsonEncode(organizers.first), + ), + colorHex: Value(collection.color), + visibility: Value( + component.classification ?? occurrence.master.classification, + ), + transparencyOrShowAs: Value( + component.transparency ?? occurrence.master.transparency, + ), + webLink: Value( + _propertyRaw(component, 'URL') ?? + _propertyRaw(occurrence.master, 'URL'), + ), + attachmentsJson: Value( + jsonEncode( + _propertyRawValues(component, 'ATTACH').isEmpty + ? _propertyRawValues(occurrence.master, 'ATTACH') + : _propertyRawValues(component, 'ATTACH'), + ), + ), + isCancelled: Value(occurrence.isCancelled), + isDeleted: const Value(false), + rawJson: Value(projectionJson), + createdAtServer: Value( + _storageTemporalNullable( + component.created ?? occurrence.master.created, + ), + ), + updatedAtServer: Value( + _storageTemporalNullable( + component.lastModified ?? occurrence.master.lastModified, + ), + ), + createdAtLocal: now, + updatedAtLocal: now, + syncStatus: const Value('synced'), + baselineRawJson: Value(projectionJson), + ), + ); + } + } + + Future _projectTasks({ + required DavCollectionCommit commit, + required DavCollection collection, + required String objectId, + required String? etag, + required IcalSemanticDocument semantic, + required Map componentIds, + }) async { + final taskListId = 'dav-task-list-${commit.collectionId}'; + final taskList = + await (_database.select(_database.taskLists)..where( + (row) => + row.accountId.equals(commit.accountId) & + row.id.equals(taskListId), + )) + .getSingleOrNull(); + if (taskList == null) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavTaskProjectionListMissing', + safeMessage: 'The task-list projection source was missing.', + ); + } + final master = semantic.components.firstWhere( + (component) => component.recurrenceId == null, + ); + final now = commit.completedAtUtc.toUtc().toIso8601String(); + for (final component in semantic.components) { + final componentId = + componentIds[_componentKey( + component.componentType, + component.uid!, + component.recurrenceIdKey, + )]!; + final taskId = _stableProjectionId( + 'dav-task', + '$objectId\u0000${component.recurrenceIdKey ?? 'master'}', + ); + final due = component.due ?? (component == master ? null : master.due); + final start = + component.start ?? (component == master ? null : master.start); + final completed = + component.completed ?? + (component.taskUiState == IcalTaskUiState.completed + ? master.completed + : null); + final extensions = { + ...master.extensionProperties, + ...component.extensionProperties, + }; + final alarms = component.alarms.isEmpty + ? master.alarms + : component.alarms; + final reminder = _taskReminderProjection(alarms, start: start, due: due); + final metadata = { + 'transport': 'caldav', + 'uid': component.uid, + if (start != null) 'nativeStart': _temporalJson(start), + if (due != null) 'nativeDue': _temporalJson(due), + if (component.recurrenceId != null) + 'recurrenceId': _temporalJson(component.recurrenceId!), + 'alarms': _alarmProjection(alarms), + }; + final taskStatus = switch (component.taskUiState) { + IcalTaskUiState.completed => 'completed', + IcalTaskUiState.inProgress => 'inProcess', + IcalTaskUiState.open => 'needsAction', + IcalTaskUiState.cancelled => 'cancelled', + }; + final priority = component.priority ?? master.priority; + final sortOrder = nextcloudTaskSortOrder( + component, + fallback: component == master ? null : master, + ); + await _database + .into(_database.tasks) + .insertOnConflictUpdate( + TasksCompanion.insert( + accountId: commit.accountId, + taskListId: taskListId, + id: taskId, + davCollectionId: Value(commit.collectionId), + davObjectId: Value(objectId), + davComponentId: Value(componentId), + icalUid: Value(component.uid), + recurrenceIdKey: Value(component.recurrenceIdKey), + icalPriority: Value(priority), + percentComplete: Value( + component.percentComplete ?? master.percentComplete, + ), + taskLocation: Value(component.location ?? master.location), + taskUrl: Value(component.url ?? master.url), + taskClassification: Value( + component.classification ?? master.classification, + ), + taskPinned: Value(_extensionFlag(extensions, 'X-PINNED')), + taskHideSubtasks: Value( + _extensionFlag(extensions, 'X-OC-HIDESUBTASKS'), + ), + taskHideCompletedSubtasks: Value( + _extensionFlag(extensions, 'X-OC-HIDECOMPLETEDSUBTASKS'), + ), + taskAlarmsJson: Value(jsonEncode(_alarmProjection(alarms))), + parentUid: Value(component.parentUid ?? master.parentUid), + sortOrder: Value(sortOrder), + providerExtensionProjectionJson: Value(jsonEncode(extensions)), + projectionVersion: const Value(davProjectionVersion), + kind: const Value('tasks#task'), + etag: Value(etag), + title: component.summary ?? master.summary ?? '', + updatedUtc: Value( + _storageTemporalNullable( + component.lastModified ?? master.lastModified, + ), + ), + parent: Value(component.parentUid ?? master.parentUid), + position: Value('$sortOrder'), + notes: Value(component.description ?? master.description), + status: Value(taskStatus), + dueUtc: Value(_storageTemporalNullable(due ?? start)), + microsoftReminderDateTime: Value(reminder?.dateTime), + microsoftReminderTimeZone: Value(reminder?.timeZone), + microsoftIsReminderOn: Value(reminder != null), + completedUtc: Value(_storageTemporalNullable(completed)), + providerStatus: Value(component.status ?? master.status), + recurrenceJson: Value( + jsonEncode({ + 'rules': master.recurrenceRules, + 'dates': master.recurrenceDates, + 'excludedDates': master.exceptionDates, + }), + ), + importance: Value(_importanceForIcalPriority(priority)), + categoriesJson: Value( + jsonEncode( + component.categories.isEmpty + ? master.categories + : component.categories, + ), + ), + providerMetadataJson: Value(jsonEncode(metadata)), + deleted: const Value(false), + hidden: const Value(false), + rawJson: jsonEncode(metadata), + serverMissing: const Value(false), + localDirty: const Value(false), + pendingDelete: const Value(false), + pendingMove: const Value(false), + localCreated: const Value(false), + lastSyncedAtUtc: Value(now), + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); + } + } + + Future _markObjectDeleted( + DavObject object, + DavCollectionCommit commit, { + bool ignorePendingOperations = false, + }) async { + final now = commit.completedAtUtc.toUtc().toIso8601String(); + await (_database.update( + _database.davObjects, + )..where((row) => row.id.equals(object.id))).write( + DavObjectsCompanion( + serverDeleted: const Value(true), + baselineGeneration: Value(commit.baselineGeneration), + lastChangedAtUtc: Value(now), + ), + ); + if (ignorePendingOperations || + !await _hasActivePendingOperation(object.id)) { + await _deleteProjections(object.id); + } else { + await (_database.update(_database.calendarEvents) + ..where((row) => row.davObjectId.equals(object.id))) + .write(const CalendarEventsCompanion(syncStatus: Value('conflict'))); + } + } + + Future _resolveProjectedTaskParents(String collectionId) async { + final tasks = await (_database.select( + _database.tasks, + )..where((row) => row.davCollectionId.equals(collectionId))).get(); + final masterIdByUid = {}; + for (final task in tasks) { + final uid = task.icalUid; + if (uid != null && task.recurrenceIdKey == null) { + masterIdByUid[uid] = task.id; + } + } + for (final task in tasks) { + final resolvedParentId = task.parentUid == null + ? null + : masterIdByUid[task.parentUid!]; + if (task.parent == resolvedParentId) continue; + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(task.accountId) & + row.taskListId.equals(task.taskListId) & + row.id.equals(task.id), + )) + .write(TasksCompanion(parent: Value(resolvedParentId))); + } + } + + Future _hasActivePendingOperation(String objectId) async { + final pending = + await (_database.select(_database.pendingOps)..where( + (row) => + row.davObjectId.equals(objectId) & + row.state.isIn(const [ + 'pending', + 'retry', + 'in_progress', + 'blocked', + 'conflict', + 'auth_blocked', + 'permission_blocked', + ]), + )) + .getSingleOrNull(); + return pending != null; + } + + Future _deleteProjections(String objectId) async { + await (_database.delete( + _database.calendarEvents, + )..where((row) => row.davObjectId.equals(objectId))).go(); + await (_database.delete( + _database.tasks, + )..where((row) => row.davObjectId.equals(objectId))).go(); + } +} + +({DateTime start, DateTime end}) _projectionRange( + SyncCursor? cursor, + DateTime nowUtc, +) { + final fallback = ( + start: DateTime.utc(nowUtc.year - 1, nowUtc.month, nowUtc.day), + end: DateTime.utc(nowUtc.year + 2, nowUtc.month, nowUtc.day), + ); + final stateJson = cursor?.stateJson; + if (stateJson == null || stateJson.isEmpty) return fallback; + try { + final decoded = jsonDecode(stateJson); + if (decoded is! Map) return fallback; + final startRaw = decoded['projectionRangeStartUtc']; + final endRaw = decoded['projectionRangeEndUtc']; + if (startRaw is! String || endRaw is! String) return fallback; + final start = DateTime.tryParse(startRaw)?.toUtc(); + final end = DateTime.tryParse(endRaw)?.toUtc(); + if (start == null || end == null || !end.isAfter(start)) return fallback; + return (start: start, end: end); + } on FormatException { + return fallback; + } +} + +bool _hrefIsMemberOf(String memberHref, String collectionHref) { + final prefix = collectionHref.endsWith('/') + ? collectionHref + : '$collectionHref/'; + if (!memberHref.startsWith(prefix)) return false; + final relative = memberHref.substring(prefix.length); + return relative.isNotEmpty && !relative.contains('/'); +} + +String _componentKey(String type, String uid, String? recurrenceIdKey) => + '$type\u0000$uid\u0000${recurrenceIdKey ?? ''}'; + +String _stableProjectionId(String prefix, String source) => + '$prefix-${sha256.convert(utf8.encode(source))}'; + +String? _storageTemporalNullable(IcalTemporalValue? value) => + value == null ? null : _storageTemporal(value); + +String _storageTemporal(IcalTemporalValue value) { + String two(int number) => number.toString().padLeft(2, '0'); + final wall = value.localValue; + final date = + '${wall.year.toString().padLeft(4, '0')}-' + '${two(wall.month)}-${two(wall.day)}'; + if (value.kind == IcalTemporalKind.date) return date; + if (value.kind == IcalTemporalKind.utcDateTime) { + return icalTemporalToUtc(value).toIso8601String(); + } + return '${date}T${two(wall.hour)}:${two(wall.minute)}:${two(wall.second)}'; +} + +String? _timeZoneName(IcalTemporalValue value) => switch (value.kind) { + IcalTemporalKind.utcDateTime => 'UTC', + IcalTemporalKind.tzidDateTime => value.timeZoneId, + IcalTemporalKind.date || IcalTemporalKind.floatingDateTime => null, +}; + +Map _temporalJson(IcalTemporalValue value) => { + 'raw': value.rawValue, + 'kind': value.kind.name, + if (value.timeZoneId != null) 'timeZoneId': value.timeZoneId, +}; + +List> _alarmProjection(List alarms) => [ + for (final alarm in alarms) + { + 'properties': [ + for (final property in alarm.properties) + { + 'name': property.name, + 'value': property.rawValue, + if (property.parameters.isNotEmpty) + 'parameters': [ + for (final parameter in property.parameters) + {'name': parameter.name, 'values': parameter.values}, + ], + }, + ], + }, +]; + +List _eventReminderMinutes(List alarms) { + final result = []; + for (final alarm in alarms) { + if (alarm.firstProperty('ACTION')?.rawValue.toUpperCase() != 'DISPLAY') { + continue; + } + final trigger = alarm.firstProperty('TRIGGER'); + if (trigger == null || + trigger.parameterValue('RELATED')?.toUpperCase() == 'END') { + continue; + } + try { + final duration = parseIcalDuration(trigger.rawValue.toUpperCase()); + if (duration == null || !duration.negative) continue; + final absolute = -duration.duration; + if (absolute.inSeconds <= 0 || absolute.inSeconds % 60 != 0) continue; + final minutes = absolute.inMinutes; + if (!result.contains(minutes)) result.add(minutes); + } on DavException { + // Absolute and unsupported trigger forms remain in the raw alarm list. + } + } + return result; +} + +({String dateTime, String? timeZone})? _taskReminderProjection( + List alarms, { + required IcalTemporalValue? start, + required IcalTemporalValue? due, +}) { + for (final alarm in alarms) { + if (alarm.firstProperty('ACTION')?.rawValue.toUpperCase() != 'DISPLAY') { + continue; + } + final trigger = alarm.firstProperty('TRIGGER'); + if (trigger == null) continue; + try { + final duration = parseIcalDuration(trigger.rawValue.toUpperCase()); + if (duration != null) { + final relatedToEnd = + trigger.parameterValue('RELATED')?.toUpperCase() == 'END'; + final reference = relatedToEnd ? due : start; + if (reference == null) continue; + final wall = reference.localValue.add(duration.duration); + final temporal = IcalTemporalValue( + rawValue: trigger.rawValue, + kind: reference.kind == IcalTemporalKind.date + ? IcalTemporalKind.floatingDateTime + : reference.kind, + localValue: wall, + timeZoneId: reference.timeZoneId, + ); + return ( + dateTime: _storageTemporal(temporal), + timeZone: _timeZoneName(temporal), + ); + } + } on DavException { + // The trigger may instead be an absolute DATE-TIME. + } + try { + final absolute = parseIcalTemporal(trigger); + if (absolute?.kind != IcalTemporalKind.utcDateTime) continue; + return (dateTime: _storageTemporal(absolute!), timeZone: 'UTC'); + } on DavException { + // Unsupported alarms remain preserved but are not exposed as editable. + } + } + return null; +} + +String? _propertyRaw(IcalSemanticComponent component, String name) => + component.documentComponent.firstProperty(name)?.rawValue; + +List _propertyRawValues(IcalSemanticComponent component, String name) => + component.documentComponent + .propertiesNamed(name) + .map((property) => property.rawValue) + .toList(growable: false); + +bool _extensionFlag(Map> extensions, String name) { + final value = extensions[name]?.firstOrNull?.trim().toUpperCase(); + return value == 'TRUE' || value == '1'; +} + +String _importanceForIcalPriority(int? priority) { + if (priority != null && priority >= 1 && priority <= 4) return 'high'; + if (priority != null && priority >= 6 && priority <= 9) return 'low'; + return 'normal'; +} diff --git a/lib/src/dav/storage/dav_settings_repository.dart b/lib/src/dav/storage/dav_settings_repository.dart new file mode 100644 index 0000000..20f8b27 --- /dev/null +++ b/lib/src/dav/storage/dav_settings_repository.dart @@ -0,0 +1,305 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; + +import '../../db/app_database.dart'; +import '../../features/accounts/domain/account_connection_state.dart'; +import '../../features/tasks/domain/task_capabilities.dart'; +import '../../providers/busy_provider.dart'; +import '../../providers/provider_capabilities.dart'; +import 'dav_collection_capabilities.dart'; + +final class DavCollectionSettingsEntity { + const DavCollectionSettingsEntity({ + required this.id, + required this.accountId, + required this.provider, + required this.accountLabel, + required this.accountAuthority, + required this.connectionState, + required this.name, + required this.color, + required this.readOnly, + required this.shared, + required this.supportsEvents, + required this.supportsTasks, + required this.eventsSelected, + required this.tasksSelected, + required this.lastSyncAtUtc, + required this.syncErrorCode, + required this.capabilities, + }); + + final String id; + final String accountId; + final BusyProvider provider; + final String accountLabel; + final String accountAuthority; + final AccountConnectionState connectionState; + final String name; + final String? color; + final bool readOnly; + final bool shared; + final bool supportsEvents; + final bool supportsTasks; + final bool eventsSelected; + final bool tasksSelected; + final DateTime? lastSyncAtUtc; + final String? syncErrorCode; + final CollectionCapabilities capabilities; + + TaskCollectionCapabilities get taskCapabilities { + if (!supportsTasks) return noTaskCollectionCapabilities; + final base = nextcloudTaskCollectionCapabilities; + return TaskCollectionCapabilities( + supportsDueDate: base.supportsDueDate, + supportsDueTime: base.supportsDueTime, + supportsStartDateTime: base.supportsStartDateTime, + supportsReminderDateTime: base.supportsReminderDateTime, + supportsRecurrence: base.supportsRecurrence, + supportsImportance: base.supportsImportance, + supportsCategories: base.supportsCategories, + supportsTaskHierarchy: base.supportsTaskHierarchy, + supportsTaskReparenting: + base.supportsTaskReparenting && capabilities.canUpdateTask, + supportsCrossListMove: + base.supportsCrossListMove && capabilities.canDeleteTask, + supportsClearCompleted: + base.supportsClearCompleted && capabilities.canDeleteTask, + supportsHiddenTasks: false, + supportsAssignedTasks: false, + supportsListRename: + base.supportsListRename && capabilities.canWriteProperties, + supportsListDelete: + base.supportsListDelete && (!capabilities.isReadOnly || shared), + canCreateTasks: capabilities.canCreateTask, + canUpdateTasks: capabilities.canUpdateTask, + canDeleteTasks: capabilities.canDeleteTask, + supportsIcalPriority: base.supportsIcalPriority, + supportsPercentComplete: base.supportsPercentComplete, + supportsTaskStatus: base.supportsTaskStatus, + supportsCompletedDateTime: base.supportsCompletedDateTime, + supportsLocation: base.supportsLocation, + supportsUrl: base.supportsUrl, + supportsClassification: base.supportsClassification, + supportsMultipleReminders: base.supportsMultipleReminders, + supportsAdvancedRecurrence: base.supportsAdvancedRecurrence, + supportsPinning: base.supportsPinning, + supportsSubtaskVisibility: base.supportsSubtaskVisibility, + supportsDuplicate: base.supportsDuplicate, + supportsNativeExport: base.supportsNativeExport, + canUpdateClassification: !shared, + ); + } +} + +/// Manages local visibility for discovered DAV collections. +final class DavSettingsRepository { + DavSettingsRepository({ + required AppDatabase database, + Future Function(String accountId)? onVisibilityChanged, + }) : _database = database, + _onVisibilityChanged = onVisibilityChanged; + + final AppDatabase _database; + final Future Function(String)? _onVisibilityChanged; + + Stream> watchCollections() { + final query = + _database.select(_database.davCollections).join([ + innerJoin( + _database.accounts, + _database.accounts.id.equalsExp( + _database.davCollections.accountId, + ), + ), + leftOuterJoin( + _database.davAccountServices, + _database.davAccountServices.accountId.equalsExp( + _database.davCollections.accountId, + ), + ), + leftOuterJoin( + _database.syncCursors, + _database.syncCursors.davCollectionId.equalsExp( + _database.davCollections.id, + ) & + _database.syncCursors.transport.equals('caldav'), + ), + ]) + ..where( + _database.davCollections.deleted.equals(false) & + _database.davCollections.serverMissing.equals(false) & + _database.accounts.provider.isIn(const [ + 'apple_icloud', + 'nextcloud', + ]), + ) + ..orderBy([ + OrderingTerm.asc(_database.accounts.provider), + OrderingTerm.asc(_database.accounts.displayName), + OrderingTerm.asc(_database.davCollections.sortOrder), + OrderingTerm.asc(_database.davCollections.displayName), + ]); + return query.watch().map((rows) { + return [ + for (final row in rows) + _fromRow( + row.readTable(_database.davCollections), + row.readTable(_database.accounts), + row.readTableOrNull(_database.davAccountServices), + row.readTableOrNull(_database.syncCursors), + ), + ]; + }); + } + + Future collectionByTaskListId( + String accountId, + String taskListId, + ) async { + final list = + await (_database.select(_database.taskLists)..where( + (row) => + row.accountId.equals(accountId) & row.id.equals(taskListId), + )) + .getSingleOrNull(); + if (list?.davCollectionId == null) return null; + return collectionById(list!.davCollectionId!); + } + + Future collectionById(String id) async { + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(id))).getSingleOrNull(); + if (collection == null || collection.deleted || collection.serverMissing) { + return null; + } + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(collection.accountId))).getSingle(); + final service = + await (_database.select(_database.davAccountServices) + ..where((row) => row.accountId.equals(collection.accountId))) + .getSingleOrNull(); + final cursor = + await (_database.select(_database.syncCursors)..where( + (row) => + row.davCollectionId.equals(collection.id) & + row.transport.equals('caldav'), + )) + .getSingleOrNull(); + return _fromRow(collection, account, service, cursor); + } + + Future setEventsSelected(String collectionId, bool selected) async { + final collection = await _requiredCollection(collectionId); + if (!collection.eventProjectionEnabled) return; + await _database.transaction(() async { + await (_database.update( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).write( + DavCollectionsCompanion( + eventsSelected: Value(selected), + updatedAtUtc: Value(DateTime.now().toUtc().toIso8601String()), + ), + ); + await (_database.update( + _database.calendarSources, + )..where((row) => row.davCollectionId.equals(collectionId))).write( + CalendarSourcesCompanion( + selected: Value(selected), + updatedAtLocal: Value(DateTime.now().millisecondsSinceEpoch), + ), + ); + }); + await _onVisibilityChanged?.call(collection.accountId); + } + + Future setTasksSelected(String collectionId, bool selected) async { + final collection = await _requiredCollection(collectionId); + if (!collection.taskProjectionEnabled) return; + await (_database.update( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).write( + DavCollectionsCompanion( + tasksSelected: Value(selected), + updatedAtUtc: Value(DateTime.now().toUtc().toIso8601String()), + ), + ); + await _onVisibilityChanged?.call(collection.accountId); + } + + Future _requiredCollection(String id) { + return (_database.select(_database.davCollections)..where( + (row) => + row.id.equals(id) & + row.deleted.equals(false) & + row.serverMissing.equals(false), + )) + .getSingle(); + } +} + +DavCollectionSettingsEntity _fromRow( + DavCollection collection, + Account account, + DavAccountService? service, + SyncCursor? cursor, +) { + final capabilities = collectionCapabilitiesFromStored(collection); + final provider = BusyProviderCodec.requireStorageValue(account.provider); + final label = _accountLabel(account, provider); + return DavCollectionSettingsEntity( + id: collection.id, + accountId: account.id, + provider: provider, + accountLabel: label, + accountAuthority: account.authority, + connectionState: AccountConnectionStateCodec.parse(account.authState), + name: collection.displayName, + color: collection.color, + readOnly: capabilities.isReadOnly, + shared: _isShared(collection, service), + supportsEvents: capabilities.supportsEvents, + supportsTasks: capabilities.supportsTasks, + eventsSelected: collection.eventsSelected, + tasksSelected: collection.tasksSelected, + lastSyncAtUtc: DateTime.tryParse(collection.lastSyncAtUtc ?? '')?.toUtc(), + syncErrorCode: cursor?.lastFailureCode ?? service?.lastDiscoveryErrorCode, + capabilities: capabilities, + ); +} + +String _accountLabel(Account account, BusyProvider provider) { + final display = account.displayName?.trim(); + if (display != null && display.isNotEmpty) return display; + final email = account.email?.trim(); + if (email != null && email.isNotEmpty) return email; + return provider.displayName; +} + +bool _isShared(DavCollection collection, DavAccountService? service) { + try { + final metadata = jsonDecode(collection.safeDisplayMetadataJson ?? '{}'); + if (metadata is Map && metadata['shared'] is bool) { + return metadata['shared']! as bool; + } + } on FormatException { + // Owner/principal comparison below remains a safe fallback. + } + final owner = _hrefPath(collection.ownerHref); + final principal = _hrefPath(service?.principalHref); + return owner != null && principal != null && owner != principal; +} + +String? _hrefPath(String? source) { + if (source == null || source.trim().isEmpty) return null; + final uri = Uri.tryParse(source.trim()); + if (uri == null) return null; + var path = uri.path; + while (path.length > 1 && path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + return path; +} diff --git a/lib/src/dav/sync/dav_account_sync_engine.dart b/lib/src/dav/sync/dav_account_sync_engine.dart new file mode 100644 index 0000000..84b1021 --- /dev/null +++ b/lib/src/dav/sync/dav_account_sync_engine.dart @@ -0,0 +1,569 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; + +import '../../core/secrets/secret_store.dart'; +import '../../db/app_database.dart'; +import '../../features/accounts/domain/account_connection_state.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../dav_provider_profile.dart'; +import '../discovery/dav_discovery_repository.dart'; +import '../discovery/dav_discovery_service.dart'; +import '../http/dav_http_transport.dart'; +import '../mutation/dav_conditional_mutation_service.dart'; +import '../mutation/dav_pending_operations.dart'; +import '../storage/dav_object_repository.dart'; +import 'dav_collection_remote_client.dart'; +import 'dav_sync_engine.dart'; + +final class DavAccountSyncPolicy { + const DavAccountSyncPolicy({ + this.discoveryMaxAge = const Duration(hours: 24), + this.inventoryMaxAge = const Duration(hours: 1), + this.maximumConcurrentCollections = 2, + }) : assert(maximumConcurrentCollections > 0); + + final Duration discoveryMaxAge; + final Duration inventoryMaxAge; + final int maximumConcurrentCollections; +} + +final class DavAccountSyncResult { + const DavAccountSyncResult({ + required this.discoveryRefreshed, + required this.collectionsSynchronized, + required this.pendingOperationsApplied, + required this.conflictsCreated, + required this.followUpCollectionsSynchronized, + required this.affectedObjectIds, + }); + + final bool discoveryRefreshed; + final int collectionsSynchronized; + final int pendingOperationsApplied; + final int conflictsCreated; + final int followUpCollectionsSynchronized; + final Set affectedObjectIds; +} + +final class DavAccountSyncException implements Exception { + DavAccountSyncException(Iterable failures) + : failures = List.unmodifiable(failures); + + final List failures; + + @override + String toString() => + 'DavAccountSyncException(codes: ' + '${failures.map((failure) => failure.code).join(',')})'; +} + +/// Coordinates discovery, synchronization, pending writes, and notification +/// rebuilds for one DAV account. +final class DavAccountSyncEngine { + DavAccountSyncEngine({ + required AppDatabase database, + required SecretStore secretStore, + required http.Client httpClient, + required String accountId, + DavAccountSyncPolicy policy = const DavAccountSyncPolicy(), + DavTransportLimits transportLimits = const DavTransportLimits(), + DavSyncLimits syncLimits = const DavSyncLimits(), + Future Function(String accountId, Set affectedObjectIds)? + rebuildNotifications, + Future Function(String accountId, DavException error)? + reportPendingMutationFailure, + DateTime Function()? nowUtc, + String Function()? correlationIdFactory, + }) : _database = database, + _secretStore = secretStore, + _httpClient = httpClient, + _accountId = accountId, + _policy = policy, + _transportLimits = transportLimits, + _syncLimits = syncLimits, + _rebuildNotifications = rebuildNotifications, + _reportPendingMutationFailure = reportPendingMutationFailure, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()), + _correlationIdFactory = correlationIdFactory ?? const Uuid().v4; + + final AppDatabase _database; + final SecretStore _secretStore; + final http.Client _httpClient; + final String _accountId; + final DavAccountSyncPolicy _policy; + final DavTransportLimits _transportLimits; + final DavSyncLimits _syncLimits; + final Future Function(String, Set)? _rebuildNotifications; + final Future Function(String, DavException)? + _reportPendingMutationFailure; + final DateTime Function() _nowUtc; + final String Function() _correlationIdFactory; + + Future? _activeSync; + + Future synchronize({ + bool full = false, + DavCancellationToken? cancellationToken, + }) { + if (_activeSync != null) return _activeSync!; + final operation = _synchronize( + full: full, + cancellationToken: cancellationToken ?? DavCancellationToken(), + ); + _activeSync = operation; + unawaited( + operation.then( + (_) => _clearActive(operation), + onError: (_, _) => _clearActive(operation), + ), + ); + return operation; + } + + Future _synchronize({ + required bool full, + required DavCancellationToken cancellationToken, + }) async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingleOrNull(); + if (account == null) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavAccountRemoved', + safeMessage: 'The DAV account is no longer available.', + ); + } + final provider = BusyProviderCodec.requireStorageValue(account.provider); + if (provider != BusyProvider.appleICloud && + provider != BusyProvider.nextcloud) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavUnsupportedAccountProvider', + safeMessage: 'This account does not use the DAV transport.', + ); + } + late final _DavCredentialContext loaded; + try { + loaded = await _loadCredential(account, provider); + } on SecretStoreException catch (error) { + final mapped = _mapSecretStoreFailure(error); + await _setConnectionState( + _requiresReconnect(mapped) + ? AccountConnectionState.reauthenticationRequired + : AccountConnectionState.temporarilyUnavailable, + ); + throw mapped; + } + final profile = davProviderProfile( + provider, + nextcloudServer: provider == BusyProvider.nextcloud + ? loaded.authority + : null, + ); + final transport = DavHttpTransport( + client: _httpClient, + profile: profile, + accountAuthority: loaded.authority, + limits: _transportLimits, + ); + final objectRepository = DavObjectRepository(database: _database); + final discoveryRepository = DavDiscoveryRepository(database: _database); + var discoveryRefreshed = false; + final affected = {}; + final failures = []; + + try { + cancellationToken.throwIfCancelled(); + if (full || await _discoveryIsDue()) { + discoveryRefreshed = true; + final discovery = + await DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: loaded.authority, + accountId: _accountId, + credential: loaded.basic, + nowUtc: _nowUtc, + ).discover( + correlationId: _correlationIdFactory(), + cancellationToken: cancellationToken, + ); + await discoveryRepository.commitSuccessfulInventory(discovery); + } + + final collections = await _selectedCollections(); + final initialResults = await _synchronizeCollections( + collections, + provider: provider, + profile: profile, + authority: loaded.authority, + credential: loaded.basic, + transport: transport, + objectRepository: objectRepository, + forceRebaseline: full, + cancellationToken: cancellationToken, + ); + for (final result in initialResults.results) { + affected.addAll(result.affectedObjectIds); + } + failures.addAll(initialResults.failures); + + final hasBlockingFailure = failures.any( + (failure) => switch (failure.category) { + DavErrorCategory.davAuthRejected || + DavErrorCategory.davCredentialsRevoked || + DavErrorCategory.davPermissionDenied || + DavErrorCategory.davReadOnly => true, + _ => false, + }, + ); + DavReplaySummary replay = const DavReplaySummary( + appliedCount: 0, + conflictCount: 0, + retryCount: 0, + mutatedCollectionIds: {}, + affectedObjectIds: {}, + paused: false, + ); + if (!hasBlockingFailure) { + replay = await DavPendingOperationsReplayer( + database: _database, + accountId: _accountId, + objectRepository: objectRepository, + nowUtc: _nowUtc, + idFactory: _correlationIdFactory, + onPermanentFailure: (operation, error) async { + await _reportPendingMutationFailure?.call(_accountId, error); + }, + serviceFactory: ({required account, required collection}) async => + DavConditionalMutationService( + remoteClient: DavMutationHttpClient( + transport: transport, + accountId: _accountId, + collectionId: collection.id, + credential: loaded.basic, + ), + nowUtc: _nowUtc, + ), + ).replayDueOperations(); + affected.addAll(replay.affectedObjectIds); + } + + var followUpCount = 0; + if (!replay.paused && replay.mutatedCollectionIds.isNotEmpty) { + final currentById = { + for (final collection in await _selectedCollections()) + collection.id: collection, + }; + final followUp = [ + for (final id in replay.mutatedCollectionIds) + if (currentById[id] != null) currentById[id]!, + ]; + final followUpResults = await _synchronizeCollections( + followUp, + provider: provider, + profile: profile, + authority: loaded.authority, + credential: loaded.basic, + transport: transport, + objectRepository: objectRepository, + forceRebaseline: false, + cancellationToken: cancellationToken, + ); + followUpCount = followUpResults.results.length; + for (final result in followUpResults.results) { + affected.addAll(result.affectedObjectIds); + } + failures.addAll(followUpResults.failures); + } + + if (affected.isNotEmpty) { + await _rebuildNotifications?.call(_accountId, affected); + } + if (failures.isNotEmpty) { + await _recordFailureState(failures.first, discoveryRepository); + throw DavAccountSyncException(failures); + } + if (!replay.paused) { + await _markSuccessful(full: full); + } + return DavAccountSyncResult( + discoveryRefreshed: discoveryRefreshed, + collectionsSynchronized: initialResults.results.length, + pendingOperationsApplied: replay.appliedCount, + conflictsCreated: replay.conflictCount, + followUpCollectionsSynchronized: followUpCount, + affectedObjectIds: Set.unmodifiable(affected), + ); + } on DavAccountSyncException { + rethrow; + } on DavException catch (error) { + await _recordFailureState(error, discoveryRepository); + rethrow; + } on SecretStoreException catch (error) { + final mapped = _mapSecretStoreFailure(error); + await _setConnectionState( + _requiresReconnect(mapped) + ? AccountConnectionState.reauthenticationRequired + : AccountConnectionState.temporarilyUnavailable, + ); + throw mapped; + } + } + + Future<_DavCredentialContext> _loadCredential( + Account account, + BusyProvider provider, + ) async { + final secret = await _secretStore.readCredential(_accountId); + if (provider == BusyProvider.appleICloud && + secret is AppleICloudSecretRecord) { + if (secret.username != account.providerAccountId) { + throw SecretStoreCredentialMismatchException( + accountId: _accountId, + expectedProvider: provider, + actualProvider: secret.provider, + actualKind: secret.kind, + ); + } + return _DavCredentialContext( + authority: Uri.parse(account.authority), + basic: DavBasicCredential( + username: secret.username, + password: secret.appSpecificPassword, + ), + ); + } + if (provider == BusyProvider.nextcloud && secret is NextcloudSecretRecord) { + final authority = Uri.parse(account.authority); + if (secret.canonicalServer != authority || + secret.loginName != account.providerAccountId) { + throw SecretStoreCredentialMismatchException( + accountId: _accountId, + expectedProvider: provider, + actualProvider: secret.provider, + actualKind: secret.kind, + ); + } + return _DavCredentialContext( + authority: authority, + basic: DavBasicCredential( + username: secret.loginName, + password: secret.appPassword, + ), + ); + } + await _setConnectionState(AccountConnectionState.reauthenticationRequired); + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavCredentialsRevoked', + safeMessage: 'The DAV account credential is unavailable.', + ); + } + + DavException _mapSecretStoreFailure(SecretStoreException error) { + if (error.code == 'SecretStoreUnavailable') { + return const DavException( + kind: DavErrorKind.network, + code: 'DavCredentialStoreUnavailable', + safeMessage: + 'Secure credential storage is temporarily unavailable. Unlock ' + 'the system keyring and try again.', + ); + } + return const DavException( + kind: DavErrorKind.authentication, + code: 'DavCredentialsRevoked', + safeMessage: 'The DAV account credential is unavailable or invalid.', + ); + } + + Future _discoveryIsDue() async { + final service = await (_database.select( + _database.davAccountServices, + )..where((row) => row.accountId.equals(_accountId))).getSingleOrNull(); + if (service == null || + service.providerProfileVersion != davProviderProfileVersion || + service.lastDiscoveryErrorCode != null) { + return true; + } + final validated = DateTime.tryParse( + service.lastValidatedAtUtc ?? service.discoveredAtUtc, + )?.toUtc(); + if (validated == null || + _nowUtc().toUtc().difference(validated) >= _policy.discoveryMaxAge) { + return true; + } + final collections = await (_database.select( + _database.davCollections, + )..where((row) => row.accountId.equals(_accountId))).get(); + if (collections.isEmpty) return true; + final oldestInventory = collections + .map( + (collection) => + DateTime.tryParse(collection.lastInventoryAtUtc ?? ''), + ) + .whereType() + .fold( + null, + (oldest, value) => + oldest == null || value.isBefore(oldest) ? value : oldest, + ); + return oldestInventory == null || + _nowUtc().toUtc().difference(oldestInventory.toUtc()) >= + _policy.inventoryMaxAge; + } + + Future> _selectedCollections() { + return (_database.select(_database.davCollections)..where( + (row) => + row.accountId.equals(_accountId) & + row.deleted.equals(false) & + row.serverMissing.equals(false) & + ((row.eventProjectionEnabled.equals(true) & + row.eventsSelected.equals(true)) | + (row.taskProjectionEnabled.equals(true) & + row.tasksSelected.equals(true))), + )) + .get(); + } + + Future<_CollectionBatchResult> _synchronizeCollections( + List collections, { + required BusyProvider provider, + required DavProviderProfile profile, + required Uri authority, + required DavBasicCredential credential, + required DavHttpTransport transport, + required DavObjectRepository objectRepository, + required bool forceRebaseline, + required DavCancellationToken cancellationToken, + }) async { + if (collections.isEmpty) return const _CollectionBatchResult(); + var nextIndex = 0; + final results = []; + final failures = []; + + Future worker() async { + while (true) { + cancellationToken.throwIfCancelled(); + if (nextIndex >= collections.length) return; + final collection = collections[nextIndex]; + nextIndex += 1; + try { + final remote = DavCollectionHttpClient( + transport: transport, + profile: profile, + accountAuthority: authority, + accountId: _accountId, + collectionId: collection.id, + collectionUri: Uri.parse(collection.requestUri), + credential: credential, + ); + final result = + await DavSyncEngine( + database: _database, + objectRepository: objectRepository, + remoteClient: remote, + accountId: _accountId, + collectionId: collection.id, + provider: provider, + limits: _syncLimits, + nowUtc: _nowUtc, + ).synchronize( + correlationId: _correlationIdFactory(), + cancellationToken: cancellationToken, + forceRebaseline: forceRebaseline, + ); + results.add(result); + } on DavException catch (error) { + failures.add(error); + } + } + } + + final workerCount = + collections.length < _policy.maximumConcurrentCollections + ? collections.length + : _policy.maximumConcurrentCollections; + await Future.wait([for (var i = 0; i < workerCount; i += 1) worker()]); + return _CollectionBatchResult(results: results, failures: failures); + } + + Future _recordFailureState( + DavException error, + DavDiscoveryRepository discoveryRepository, + ) async { + await discoveryRepository.recordDiscoveryFailure(_accountId, error.code); + final state = switch (error.category) { + DavErrorCategory.davAuthRejected || + DavErrorCategory.davCredentialsRevoked => + AccountConnectionState.reauthenticationRequired, + DavErrorCategory.davPermissionDenied || + DavErrorCategory.davReadOnly => AccountConnectionState.permissionChanged, + DavErrorCategory.davUnsupportedServer || + DavErrorCategory.davProtocolViolation || + DavErrorCategory.davUnsupportedComponent => + AccountConnectionState.unsupportedServerProfile, + _ => AccountConnectionState.temporarilyUnavailable, + }; + await _setConnectionState(state); + } + + Future _markSuccessful({required bool full}) { + final now = _nowUtc().toUtc().toIso8601String(); + return (_database.update( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).write( + AccountsCompanion( + authState: Value(AccountConnectionState.connected.storageValue), + lastSuccessfulSyncAtUtc: Value(now), + lastFullSyncAtUtc: full ? Value(now) : const Value.absent(), + updatedAtUtc: Value(now), + ), + ); + } + + Future _setConnectionState(AccountConnectionState state) { + return (_database.update( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).write( + AccountsCompanion( + authState: Value(state.storageValue), + updatedAtUtc: Value(_nowUtc().toUtc().toIso8601String()), + ), + ); + } + + void _clearActive(Future operation) { + if (identical(_activeSync, operation)) _activeSync = null; + } +} + +bool _requiresReconnect(DavException error) => switch (error.category) { + DavErrorCategory.davAuthRejected || + DavErrorCategory.davCredentialsRevoked => true, + _ => false, +}; + +final class _DavCredentialContext { + const _DavCredentialContext({required this.authority, required this.basic}); + + final Uri authority; + final DavBasicCredential basic; +} + +final class _CollectionBatchResult { + const _CollectionBatchResult({ + this.results = const [], + this.failures = const [], + }); + + final List results; + final List failures; +} diff --git a/lib/src/dav/sync/dav_collection_remote_client.dart b/lib/src/dav/sync/dav_collection_remote_client.dart new file mode 100644 index 0000000..e4fd102 --- /dev/null +++ b/lib/src/dav/sync/dav_collection_remote_client.dart @@ -0,0 +1,578 @@ +import 'dart:typed_data'; + +import '../dav_errors.dart'; +import '../dav_href.dart'; +import '../dav_provider_profile.dart'; +import '../http/dav_http_transport.dart'; +import '../xml/dav_xml.dart'; + +final class DavRemoteMember { + const DavRemoteMember({ + required this.hrefKey, + required this.requestUri, + required this.etag, + }); + + final String hrefKey; + final Uri requestUri; + final String etag; +} + +final class DavSyncPage { + const DavSyncPage({ + required this.changedMembers, + required this.deletedHrefKeys, + required this.nextSyncToken, + required this.truncated, + }); + + final List changedMembers; + final Set deletedHrefKeys; + final String nextSyncToken; + final bool truncated; +} + +final class DavMemberInventory { + const DavMemberInventory({required this.members}); + + final List members; +} + +final class DavFetchedMember { + const DavFetchedMember._({ + required this.hrefKey, + required this.requestUri, + required this.missing, + required this.etag, + required this.contentType, + required this.rawIcsBody, + }); + + const DavFetchedMember.live({ + required String hrefKey, + required Uri requestUri, + required String etag, + required String? contentType, + required String rawIcsBody, + }) : this._( + hrefKey: hrefKey, + requestUri: requestUri, + missing: false, + etag: etag, + contentType: contentType, + rawIcsBody: rawIcsBody, + ); + + const DavFetchedMember.missing({ + required String hrefKey, + required Uri requestUri, + }) : this._( + hrefKey: hrefKey, + requestUri: requestUri, + missing: true, + etag: null, + contentType: null, + rawIcsBody: null, + ); + + final String hrefKey; + final Uri requestUri; + final bool missing; + final String? etag; + final String? contentType; + final String? rawIcsBody; +} + +abstract interface class DavCollectionRemoteClient { + Future syncCollectionPage({ + required String syncToken, + required String correlationId, + DavCancellationToken? cancellationToken, + }); + + Future listMemberEtags({ + required String correlationId, + DavCancellationToken? cancellationToken, + }); + + Future> fetchMembers( + List members, { + required String correlationId, + required bool useCalendarMultiget, + DavCancellationToken? cancellationToken, + }); +} + +final class DavCollectionHttpClient implements DavCollectionRemoteClient { + DavCollectionHttpClient({ + required DavHttpTransport transport, + required DavProviderProfile profile, + required Uri accountAuthority, + required String accountId, + required String collectionId, + required Uri collectionUri, + required DavBasicCredential credential, + DavXmlParser xmlParser = const DavXmlParser(), + }) : _transport = transport, + _profile = profile, + _accountAuthority = accountAuthority, + _accountId = accountId, + _collectionId = collectionId, + _collectionUri = collectionUri, + _credential = credential, + _xmlParser = xmlParser; + + final DavHttpTransport _transport; + final DavProviderProfile _profile; + final Uri _accountAuthority; + final String _accountId; + final String _collectionId; + final Uri _collectionUri; + final DavBasicCredential _credential; + final DavXmlParser _xmlParser; + + @override + Future syncCollectionPage({ + required String syncToken, + required String correlationId, + DavCancellationToken? cancellationToken, + }) async { + final response = await _transport.send( + DavRequest.xml( + method: 'REPORT', + uri: _collectionUri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: const {'depth': '1'}, + body: _syncCollectionBody(syncToken), + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + if (response.statusCode != 207 && response.statusCode != 507) { + throw _statusException(response, operation: 'synchronize'); + } + final multistatus = _xmlParser.parseMultistatus( + response.bodyBytes, + correlationId: correlationId, + ); + if (multistatus.hasCondition(davNamespace, 'valid-sync-token')) { + throw DavException( + kind: DavErrorKind.invalidSyncToken, + code: 'DavSyncTokenInvalid', + safeMessage: 'The DAV synchronization token is no longer valid.', + statusCode: response.statusCode, + correlationId: correlationId, + ); + } + final nextToken = multistatus.syncToken; + if (nextToken == null || nextToken.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavSyncResponseMissingToken', + safeMessage: 'The DAV synchronization response omitted its token.', + correlationId: correlationId, + ); + } + final changed = []; + final deleted = {}; + var truncated = + response.statusCode == 507 || + multistatus.hasCondition( + davNamespace, + 'number-of-matches-within-limits', + ); + for (final entry in multistatus.responses) { + final target = _resolveMember(entry.href, response, correlationId); + if (target == null) continue; + final hasLimitStatus = + entry.statusCode == 507 || + entry.propstats.any((propstat) => propstat.statusCode == 507); + if (hasLimitStatus) { + truncated = true; + continue; + } + final resourceMissing = + entry.statusCode == 404 || + (entry.statusCode == null && + entry.propstats.isNotEmpty && + entry.propstats.every((propstat) => propstat.statusCode == 404)); + if (resourceMissing) { + deleted.add(target.hrefKey); + continue; + } + if (entry.statusCode case final status? when status >= 400) { + throw _memberStatusException(status, correlationId); + } + final etag = entry + .successfulProperty(davNamespace, 'getetag') + ?.text + .trim(); + if (etag == null || etag.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavSyncMemberMissingEtag', + safeMessage: 'A changed DAV object did not contain an ETag.', + correlationId: correlationId, + ); + } + changed.add( + DavRemoteMember( + hrefKey: target.hrefKey, + requestUri: target.uri, + etag: etag, + ), + ); + } + return DavSyncPage( + changedMembers: List.unmodifiable(changed), + deletedHrefKeys: Set.unmodifiable(deleted), + nextSyncToken: nextToken, + truncated: truncated, + ); + } + + @override + Future listMemberEtags({ + required String correlationId, + DavCancellationToken? cancellationToken, + }) async { + final response = await _transport.send( + DavRequest.xml( + method: 'PROPFIND', + uri: _collectionUri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: const {'depth': '1'}, + body: _memberEtagPropfind, + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + if (response.statusCode != 207) { + throw _statusException(response, operation: 'list members'); + } + final multistatus = _xmlParser.parseMultistatus( + response.bodyBytes, + correlationId: correlationId, + ); + final members = []; + for (final entry in multistatus.responses) { + final target = _resolveMember(entry.href, response, correlationId); + if (target == null) continue; + if (entry.statusCode == 404) continue; + if (entry.statusCode case final status? when status >= 400) { + throw _memberStatusException(status, correlationId); + } + final etag = entry + .successfulProperty(davNamespace, 'getetag') + ?.text + .trim(); + if (etag == null || etag.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavInventoryMemberMissingEtag', + safeMessage: 'A DAV collection member did not contain an ETag.', + correlationId: correlationId, + ); + } + members.add( + DavRemoteMember( + hrefKey: target.hrefKey, + requestUri: target.uri, + etag: etag, + ), + ); + } + return DavMemberInventory(members: List.unmodifiable(members)); + } + + @override + Future> fetchMembers( + List members, { + required String correlationId, + required bool useCalendarMultiget, + DavCancellationToken? cancellationToken, + }) async { + if (members.isEmpty) return const []; + if (!useCalendarMultiget) { + return Future.wait([ + for (final member in members) + _getMember( + member, + correlationId: correlationId, + cancellationToken: cancellationToken, + ), + ]); + } + final response = await _transport.send( + DavRequest.xml( + method: 'REPORT', + uri: _collectionUri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: const {'depth': '1'}, + body: _calendarMultigetBody(members), + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + if (response.statusCode != 207) { + throw _statusException(response, operation: 'fetch members'); + } + final multistatus = _xmlParser.parseMultistatus( + response.bodyBytes, + correlationId: correlationId, + ); + final requested = {for (final member in members) member.hrefKey: member}; + final returned = {}; + for (final entry in multistatus.responses) { + final target = _resolveMember(entry.href, response, correlationId); + if (target == null || !requested.containsKey(target.hrefKey)) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavMultigetReturnedUnexpectedMember', + safeMessage: 'The DAV server returned an unexpected object.', + correlationId: correlationId, + ); + } + if (entry.statusCode == 404) { + returned[target.hrefKey] = DavFetchedMember.missing( + hrefKey: target.hrefKey, + requestUri: target.uri, + ); + continue; + } + if (entry.statusCode case final status? when status >= 400) { + throw _memberStatusException(status, correlationId); + } + final etag = entry + .successfulProperty(davNamespace, 'getetag') + ?.text + .trim(); + final calendarData = entry.successfulProperty( + caldavNamespace, + 'calendar-data', + ); + if (etag == null || etag.isEmpty || calendarData == null) { + final onlyMissingPropstats = + entry.propstats.isNotEmpty && + entry.propstats.every((propstat) => propstat.statusCode == 404); + if (onlyMissingPropstats) { + returned[target.hrefKey] = DavFetchedMember.missing( + hrefKey: target.hrefKey, + requestUri: target.uri, + ); + continue; + } + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavMultigetMemberDataMissing', + safeMessage: 'A DAV object response omitted calendar data or ETag.', + correlationId: correlationId, + ); + } + returned[target.hrefKey] = DavFetchedMember.live( + hrefKey: target.hrefKey, + requestUri: target.uri, + etag: etag, + contentType: calendarData.element.getAttribute('content-type'), + rawIcsBody: calendarData.text, + ); + } + + // An omitted multiget member is ambiguous, so resolve it with a safe GET + // rather than treating an incomplete response as deletion. + for (final member in members) { + returned[member.hrefKey] ??= await _getMember( + member, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + } + return [for (final member in members) returned[member.hrefKey]!]; + } + + Future _getMember( + DavRemoteMember member, { + required String correlationId, + required DavCancellationToken? cancellationToken, + }) async { + final response = await _transport.send( + DavRequest( + method: 'GET', + uri: member.requestUri, + accountId: _accountId, + collectionId: _collectionId, + correlationId: correlationId, + headers: const {'accept': 'text/calendar'}, + retryClass: DavRetryClass.safeRead, + ), + credential: _credential, + cancellationToken: cancellationToken, + ); + if (response.statusCode == 404) { + return DavFetchedMember.missing( + hrefKey: member.hrefKey, + requestUri: response.requestUri, + ); + } + if (response.statusCode < 200 || response.statusCode >= 300) { + throw _statusException(response, operation: 'fetch object'); + } + final etag = response.etag; + if (etag == null || etag.isEmpty) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavGetMemberMissingEtag', + safeMessage: 'A DAV object response omitted its ETag.', + correlationId: correlationId, + ); + } + return DavFetchedMember.live( + hrefKey: member.hrefKey, + requestUri: response.requestUri, + etag: etag, + contentType: response.headers['content-type'], + rawIcsBody: response.bodyText, + ); + } + + ({String hrefKey, Uri uri})? _resolveMember( + String href, + DavResponse response, + String correlationId, + ) { + final uri = resolveDavHref( + href: href, + responseRequestUri: response.requestUri, + profile: _profile, + accountAuthority: _accountAuthority, + correlationId: correlationId, + ); + final key = normalizedDavHrefKey(_profile.provider, uri); + final collectionKey = normalizedDavHrefKey( + _profile.provider, + _collectionUri, + ); + if (_sameCollectionTarget(key, collectionKey)) return null; + if (!_isDirectMember(key, collectionKey)) { + throw DavException( + kind: DavErrorKind.protocol, + code: 'DavObjectOutsideCollection', + safeMessage: + 'A DAV response referenced an object outside its collection.', + correlationId: correlationId, + ); + } + return (hrefKey: key, uri: uri); + } + + DavException _statusException( + DavResponse response, { + required String operation, + }) { + final conditions = _tryErrorConditions( + response.bodyBytes, + response.correlationId, + ); + if (conditions.contains( + const DavPropertyName(davNamespace, 'valid-sync-token'), + )) { + return DavException( + kind: DavErrorKind.invalidSyncToken, + code: 'DavSyncTokenInvalid', + safeMessage: 'The DAV synchronization token is no longer valid.', + statusCode: response.statusCode, + correlationId: response.correlationId, + ); + } + final mapped = switch (response.statusCode) { + 401 => (DavErrorKind.authentication, 'DavAuthRejected'), + 403 => (DavErrorKind.authorization, 'DavPermissionDenied'), + 404 || 410 => (DavErrorKind.notFound, 'DavCollectionRemoved'), + 409 || 412 || 423 => (DavErrorKind.conflict, 'DavResourceConflict'), + 429 => (DavErrorKind.rateLimited, 'DavRateLimited'), + 507 => (DavErrorKind.limitExceeded, 'DavQuotaOrSizeLimit'), + >= 500 => (DavErrorKind.server, 'DavServerUnavailable'), + _ => (DavErrorKind.protocol, 'DavProtocolViolation'), + }; + return DavException( + kind: mapped.$1, + code: mapped.$2, + safeMessage: 'The DAV server could not $operation.', + statusCode: response.statusCode, + correlationId: response.correlationId, + retryAfter: parseDavRetryAfter(response.headers['retry-after']), + ); + } + + Set _tryErrorConditions( + Uint8List body, + String correlationId, + ) { + if (body.isEmpty) return const {}; + try { + return _xmlParser.parseDavError(body, correlationId: correlationId); + } on DavException { + return const {}; + } + } +} + +DavException _memberStatusException(int status, String correlationId) => + DavException( + kind: status == 403 + ? DavErrorKind.authorization + : status >= 500 + ? DavErrorKind.server + : DavErrorKind.protocol, + code: 'DavMemberStatus$status', + safeMessage: 'A DAV collection member returned an error status.', + statusCode: status, + correlationId: correlationId, + ); + +bool _sameCollectionTarget(String memberKey, String collectionKey) => + memberKey == collectionKey || + (collectionKey.endsWith('/') && + memberKey == collectionKey.substring(0, collectionKey.length - 1)) || + (memberKey.endsWith('/') && + memberKey.substring(0, memberKey.length - 1) == collectionKey); + +bool _isDirectMember(String memberKey, String collectionKey) { + final prefix = collectionKey.endsWith('/') + ? collectionKey + : '$collectionKey/'; + if (!memberKey.startsWith(prefix)) return false; + final remainder = memberKey.substring(prefix.length); + return remainder.isNotEmpty && !remainder.contains('/'); +} + +String _xmlText(String value) => value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + +String _syncCollectionBody(String token) => + ''' + + ${_xmlText(token)} + 1 + +'''; + +String _calendarMultigetBody(List members) => + ''' + + + ${members.map((member) => '${_xmlText(member.hrefKey)}').join()} +'''; + +const _memberEtagPropfind = ''' +'''; diff --git a/lib/src/dav/sync/dav_sync_engine.dart b/lib/src/dav/sync/dav_sync_engine.dart new file mode 100644 index 0000000..d295d70 --- /dev/null +++ b/lib/src/dav/sync/dav_sync_engine.dart @@ -0,0 +1,404 @@ +import 'dart:convert'; + +import '../../db/app_database.dart'; +import '../../providers/busy_provider.dart'; +import '../dav_errors.dart'; +import '../http/dav_http_transport.dart'; +import '../storage/dav_object_repository.dart'; +import 'dav_collection_remote_client.dart'; + +final class DavSyncLimits { + const DavSyncLimits({ + this.maximumSyncPages = 1000, + this.maximumMembersPerMultiget = 100, + this.maximumResourceBytes = 16 * 1024 * 1024, + }); + + final int maximumSyncPages; + final int maximumMembersPerMultiget; + final int maximumResourceBytes; +} + +final class DavCollectionSyncResult { + const DavCollectionSyncResult({ + required this.initialOrRebaseline, + required this.usedSyncCollection, + required this.pages, + required this.membersSeen, + required this.objectsFetched, + required this.objectsDeleted, + required this.finalCursorKind, + required this.finalCursorValue, + required this.affectedObjectIds, + }); + + final bool initialOrRebaseline; + final bool usedSyncCollection; + final int pages; + final int membersSeen; + final int objectsFetched; + final int objectsDeleted; + final String finalCursorKind; + final String finalCursorValue; + final Set affectedObjectIds; +} + +final class DavSyncEngine { + DavSyncEngine({ + required AppDatabase database, + required DavObjectRepository objectRepository, + required DavCollectionRemoteClient remoteClient, + required String accountId, + required String collectionId, + required BusyProvider provider, + DavSyncLimits limits = const DavSyncLimits(), + DateTime Function()? nowUtc, + Future Function(String accountId)? onNotificationsNeedRebuild, + Future Function(String collectionId)? onCollectionNeedsRediscovery, + }) : _database = database, + _objectRepository = objectRepository, + _remoteClient = remoteClient, + _accountId = accountId, + _collectionId = collectionId, + _provider = provider, + _limits = limits, + _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()), + _onNotificationsNeedRebuild = onNotificationsNeedRebuild, + _onCollectionNeedsRediscovery = onCollectionNeedsRediscovery; + + final AppDatabase _database; + final DavObjectRepository _objectRepository; + final DavCollectionRemoteClient _remoteClient; + final String _accountId; + final String _collectionId; + final BusyProvider _provider; + final DavSyncLimits _limits; + final DateTime Function() _nowUtc; + final Future Function(String accountId)? _onNotificationsNeedRebuild; + final Future Function(String collectionId)? + _onCollectionNeedsRediscovery; + + Future synchronize({ + required String correlationId, + DateTime? projectionRangeStartUtc, + DateTime? projectionRangeEndUtc, + DavCancellationToken? cancellationToken, + bool forceRebaseline = false, + }) async { + cancellationToken?.throwIfCancelled(correlationId: correlationId); + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(_collectionId))).getSingleOrNull(); + if (collection == null || collection.accountId != _accountId) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavCollectionNotFound', + safeMessage: 'The DAV collection is no longer available.', + ); + } + if (collection.deleted || collection.serverMissing) { + throw const DavException( + kind: DavErrorKind.notFound, + code: 'DavCollectionRemoved', + safeMessage: 'The DAV collection was removed from the server.', + ); + } + final reports = _stringSet(collection.supportedReportsJson); + final supportsSyncCollection = reports.contains('{DAV:}sync-collection'); + final supportsCalendarMultiget = reports.contains( + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ); + final cursor = await _objectRepository.cursor(_collectionId); + final generation = await _objectRepository.nextBaselineGeneration( + _collectionId, + ); + final window = davProjectionWindow(_nowUtc()); + final projectionStart = (projectionRangeStartUtc ?? window.start).toUtc(); + final projectionEnd = (projectionRangeEndUtc ?? window.end).toUtc(); + final projectionStale = _projectionStateStale( + cursor, + projectionStart, + projectionEnd, + ); + await _objectRepository.markSyncStarted( + accountId: _accountId, + collectionId: _collectionId, + provider: _provider, + generation: generation, + ); + + try { + final localObjects = await _objectRepository.liveObjects(_collectionId); + final localByHref = { + for (final object in localObjects) object.hrefKey: object, + }; + late final _CollectedRemoteChanges remote; + var initialOrRebaseline = false; + if (supportsSyncCollection) { + final savedToken = + !forceRebaseline && cursor?.cursorKind == 'dav_sync_token' + ? cursor?.cursorValue + : null; + initialOrRebaseline = savedToken == null || savedToken.isEmpty; + try { + remote = await _collectSyncPages( + syncToken: savedToken ?? '', + initial: initialOrRebaseline, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + } on DavException catch (error) { + if (error.kind != DavErrorKind.invalidSyncToken || + initialOrRebaseline) { + rethrow; + } + initialOrRebaseline = true; + remote = await _collectSyncPages( + syncToken: '', + initial: true, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + } + } else { + initialOrRebaseline = true; + remote = await _collectFallbackInventory( + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + } + + final fetch = []; + for (final member in remote.liveMembers.values) { + final local = localByHref[member.hrefKey]; + if (local == null || + local.serverDeleted || + local.etag != member.etag || + local.parserVersion != davRawObjectParserVersion) { + fetch.add(member); + } + } + final prepared = []; + final deleted = {...remote.deletedHrefKeys}; + final membership = {...remote.membershipHrefKeys}; + for ( + var offset = 0; + offset < fetch.length; + offset += _limits.maximumMembersPerMultiget + ) { + final end = (offset + _limits.maximumMembersPerMultiget).clamp( + 0, + fetch.length, + ); + final batch = fetch.sublist(offset, end); + final fetched = await _remoteClient.fetchMembers( + batch, + correlationId: correlationId, + useCalendarMultiget: supportsCalendarMultiget, + cancellationToken: cancellationToken, + ); + if (fetched.length != batch.length) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavObjectFetchIncomplete', + safeMessage: 'The DAV server returned an incomplete object batch.', + ); + } + for (final object in fetched) { + if (object.missing) { + deleted.add(object.hrefKey); + membership.remove(object.hrefKey); + continue; + } + final serverMaximum = collection.maximumResourceSize; + final maximumBytes = serverMaximum == null || serverMaximum <= 0 + ? _limits.maximumResourceBytes + : serverMaximum < _limits.maximumResourceBytes + ? serverMaximum + : _limits.maximumResourceBytes; + prepared.add( + DavPreparedObject.parse( + hrefKey: object.hrefKey, + requestUri: object.requestUri, + etag: object.etag, + contentType: object.contentType, + rawIcsBody: object.rawIcsBody!, + maximumResourceBytes: maximumBytes, + ), + ); + } + } + + final completedAt = _nowUtc().toUtc(); + final finalCursorKind = supportsSyncCollection + ? 'dav_sync_token' + : 'snapshot_generation'; + final finalCursorValue = supportsSyncCollection + ? remote.finalToken! + : generation.toString(); + final affected = await _objectRepository.commit( + DavCollectionCommit( + accountId: _accountId, + collectionId: _collectionId, + provider: _provider, + objects: prepared, + deletedHrefKeys: deleted, + completeMembership: remote.completeMembership, + membershipHrefKeys: membership, + finalCursorKind: finalCursorKind, + finalCursorValue: finalCursorValue, + baselineGeneration: generation, + completedAtUtc: completedAt, + projectionRangeStartUtc: projectionStart, + projectionRangeEndUtc: projectionEnd, + forceReprojection: projectionStale, + ), + ); + await _onNotificationsNeedRebuild?.call(_accountId); + return DavCollectionSyncResult( + initialOrRebaseline: initialOrRebaseline, + usedSyncCollection: supportsSyncCollection, + pages: remote.pages, + membersSeen: membership.length, + objectsFetched: prepared.length, + objectsDeleted: deleted.length, + finalCursorKind: finalCursorKind, + finalCursorValue: finalCursorValue, + affectedObjectIds: Set.unmodifiable(affected), + ); + } on DavException catch (error) { + await _objectRepository.markSyncFailed( + collectionId: _collectionId, + errorCode: error.code, + ); + if (error.kind == DavErrorKind.notFound) { + await _onCollectionNeedsRediscovery?.call(_collectionId); + } + rethrow; + } on Object { + await _objectRepository.markSyncFailed( + collectionId: _collectionId, + errorCode: 'DavUnexpectedSyncFailure', + ); + rethrow; + } + } + + Future<_CollectedRemoteChanges> _collectSyncPages({ + required String syncToken, + required bool initial, + required String correlationId, + required DavCancellationToken? cancellationToken, + }) async { + var requestToken = syncToken; + final seenTokens = {syncToken}; + final live = {}; + final deleted = {}; + var pages = 0; + while (pages < _limits.maximumSyncPages) { + final page = await _remoteClient.syncCollectionPage( + syncToken: requestToken, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + pages += 1; + for (final member in page.changedMembers) { + live[member.hrefKey] = member; + deleted.remove(member.hrefKey); + } + for (final href in page.deletedHrefKeys) { + live.remove(href); + deleted.add(href); + } + if (!page.truncated) { + return _CollectedRemoteChanges( + liveMembers: live, + deletedHrefKeys: deleted, + membershipHrefKeys: initial ? live.keys.toSet() : const {}, + completeMembership: initial, + finalToken: page.nextSyncToken, + pages: pages, + ); + } + if (!seenTokens.add(page.nextSyncToken)) { + throw const DavException( + kind: DavErrorKind.protocol, + code: 'DavSyncPaginationTokenLoop', + safeMessage: 'The DAV synchronization pagination did not advance.', + ); + } + requestToken = page.nextSyncToken; + } + throw const DavException( + kind: DavErrorKind.limitExceeded, + code: 'DavSyncPageLimitExceeded', + safeMessage: 'The DAV synchronization returned too many pages.', + ); + } + + Future<_CollectedRemoteChanges> _collectFallbackInventory({ + required String correlationId, + required DavCancellationToken? cancellationToken, + }) async { + final inventory = await _remoteClient.listMemberEtags( + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + final live = { + for (final member in inventory.members) member.hrefKey: member, + }; + return _CollectedRemoteChanges( + liveMembers: live, + deletedHrefKeys: const {}, + membershipHrefKeys: live.keys.toSet(), + completeMembership: true, + finalToken: null, + pages: 1, + ); + } +} + +final class _CollectedRemoteChanges { + const _CollectedRemoteChanges({ + required this.liveMembers, + required this.deletedHrefKeys, + required this.membershipHrefKeys, + required this.completeMembership, + required this.finalToken, + required this.pages, + }); + + final Map liveMembers; + final Set deletedHrefKeys; + final Set membershipHrefKeys; + final bool completeMembership; + final String? finalToken; + final int pages; +} + +Set _stringSet(String source) { + final decoded = jsonDecode(source); + if (decoded is! List) return const {}; + return decoded.map((value) => value.toString()).toSet(); +} + +bool _projectionStateStale(SyncCursor? cursor, DateTime start, DateTime end) { + if (cursor == null || cursor.stateJson == null) return true; + try { + final decoded = jsonDecode(cursor.stateJson!); + return decoded is! Map || + decoded['projectionRangeStartUtc'] != start.toIso8601String() || + decoded['projectionRangeEndUtc'] != end.toIso8601String() || + decoded['projectionVersion'] != davProjectionVersion; + } on FormatException { + return true; + } +} + +({DateTime start, DateTime end}) davProjectionWindow(DateTime nowUtc) { + final utc = nowUtc.toUtc(); + return ( + start: DateTime.utc(utc.year - 1, utc.month), + end: DateTime.utc(utc.year + 2, utc.month + 1), + ); +} diff --git a/lib/src/dav/xml/dav_xml.dart b/lib/src/dav/xml/dav_xml.dart new file mode 100644 index 0000000..23b3c8b --- /dev/null +++ b/lib/src/dav/xml/dav_xml.dart @@ -0,0 +1,423 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:xml/xml.dart'; + +import '../dav_errors.dart'; + +const davNamespace = 'DAV:'; +const caldavNamespace = 'urn:ietf:params:xml:ns:caldav'; +const calendarServerNamespace = 'http://calendarserver.org/ns/'; +const appleIcalNamespace = 'http://apple.com/ns/ical/'; +const owncloudNamespace = 'http://owncloud.org/ns'; +const nextcloudNamespace = 'http://nextcloud.com/ns'; + +final class DavXmlLimits { + const DavXmlLimits({ + this.maximumBytes = 8 * 1024 * 1024, + this.maximumDepth = 64, + this.maximumElements = 100000, + this.maximumTextBytes = 4 * 1024 * 1024, + }); + + final int maximumBytes; + final int maximumDepth; + final int maximumElements; + final int maximumTextBytes; +} + +final class DavPropertyName { + const DavPropertyName(this.namespaceUri, this.localName); + + final String? namespaceUri; + final String localName; + + @override + bool operator ==(Object other) => + other is DavPropertyName && + other.namespaceUri == namespaceUri && + other.localName == localName; + + @override + int get hashCode => Object.hash(namespaceUri, localName); + + @override + String toString() => '{$namespaceUri}$localName'; +} + +final class DavProperty { + const DavProperty({required this.name, required this.element}); + + final DavPropertyName name; + final XmlElement element; + + String get text => element.innerText; + + Iterable get childElements => element.childElements; +} + +final class DavPropstat { + const DavPropstat({ + required this.statusCode, + required this.reasonPhrase, + required this.properties, + required this.errorConditions, + }); + + final int statusCode; + final String reasonPhrase; + final List properties; + final Set errorConditions; + + bool get isSuccessful => statusCode >= 200 && statusCode < 300; + + DavProperty? property(String namespaceUri, String localName) { + for (final property in properties) { + if (property.name == DavPropertyName(namespaceUri, localName)) { + return property; + } + } + return null; + } +} + +final class DavMultistatusResponse { + const DavMultistatusResponse({ + required this.href, + required this.propstats, + required this.statusCode, + required this.reasonPhrase, + required this.errorConditions, + }); + + final String href; + final List propstats; + final int? statusCode; + final String? reasonPhrase; + final Set errorConditions; + + bool get isMissing => + statusCode == 404 || + (statusCode == null && + propstats.isNotEmpty && + propstats.every((propstat) => propstat.statusCode == 404)); + + DavProperty? successfulProperty(String namespaceUri, String localName) { + for (final propstat in propstats.where((entry) => entry.isSuccessful)) { + final property = propstat.property(namespaceUri, localName); + if (property != null) { + return property; + } + } + return null; + } +} + +final class DavMultistatus { + const DavMultistatus({ + required this.responses, + required this.syncToken, + required this.errorConditions, + }); + + final List responses; + final String? syncToken; + final Set errorConditions; + + bool hasCondition(String namespaceUri, String localName) => + errorConditions.contains(DavPropertyName(namespaceUri, localName)) || + responses.any( + (response) => + response.errorConditions.contains( + DavPropertyName(namespaceUri, localName), + ) || + response.propstats.any( + (propstat) => propstat.errorConditions.contains( + DavPropertyName(namespaceUri, localName), + ), + ), + ); +} + +final class DavXmlParser { + const DavXmlParser({this.limits = const DavXmlLimits()}); + + final DavXmlLimits limits; + + Set parseDavError(Uint8List bytes, {String? correlationId}) { + final document = _parseDocument(bytes, correlationId); + final root = document.rootElement; + if (!_matches(root, davNamespace, 'error')) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlExpectedError', + 'The DAV server returned an invalid error document.', + correlationId, + ); + } + return Set.unmodifiable({ + for (final child in root.childElements) + DavPropertyName(child.name.namespaceUri, child.name.local), + }); + } + + DavMultistatus parseMultistatus(Uint8List bytes, {String? correlationId}) { + final document = _parseDocument(bytes, correlationId); + final root = document.rootElement; + if (!_matches(root, davNamespace, 'multistatus')) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlExpectedMultistatus', + 'The DAV server response was not a DAV multistatus document.', + correlationId, + ); + } + + final responses = []; + for (final response in _direct(root, davNamespace, 'response')) { + responses.add(_parseResponse(response, correlationId)); + } + final syncToken = _firstDirect( + root, + davNamespace, + 'sync-token', + )?.innerText.trim(); + return DavMultistatus( + responses: List.unmodifiable(responses), + syncToken: syncToken == null || syncToken.isEmpty ? null : syncToken, + errorConditions: _parseErrorConditions(root), + ); + } + + XmlDocument _parseDocument(Uint8List bytes, String? correlationId) { + if (bytes.length > limits.maximumBytes) { + throw _error( + DavErrorKind.responseTooLarge, + 'DavXmlResponseTooLarge', + 'The DAV XML response exceeded the configured size limit.', + correlationId, + ); + } + final source = _decodeUtf8(bytes, correlationId); + if (RegExp( + r'[]; + for (final propstat in _direct(element, davNamespace, 'propstat')) { + propstats.add(_parsePropstat(propstat, correlationId)); + } + final statusText = _firstDirect(element, davNamespace, 'status')?.innerText; + final status = statusText == null + ? null + : _parseStatus(statusText, correlationId); + if (status == null && propstats.isEmpty) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlResponseMissingStatus', + 'A DAV multistatus response contained no status information.', + correlationId, + ); + } + return DavMultistatusResponse( + href: href, + propstats: List.unmodifiable(propstats), + statusCode: status?.code, + reasonPhrase: status?.reason, + errorConditions: _parseErrorConditions(element), + ); + } + + DavPropstat _parsePropstat(XmlElement element, String? correlationId) { + final statusText = _firstDirect(element, davNamespace, 'status')?.innerText; + if (statusText == null) { + throw _error( + DavErrorKind.malformedStatus, + 'DavXmlPropstatMissingStatus', + 'A DAV property status did not contain an HTTP status line.', + correlationId, + ); + } + final status = _parseStatus(statusText, correlationId); + final prop = _firstDirect(element, davNamespace, 'prop'); + if (prop == null) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlPropstatMissingProp', + 'A DAV property status did not contain a property container.', + correlationId, + ); + } + final properties = [ + for (final child in prop.childElements) + DavProperty( + name: DavPropertyName(child.name.namespaceUri, child.name.local), + element: child, + ), + ]; + return DavPropstat( + statusCode: status.code, + reasonPhrase: status.reason, + properties: List.unmodifiable(properties), + errorConditions: _parseErrorConditions(element), + ); + } + + ({int code, String reason}) _parseStatus( + String source, + String? correlationId, + ) { + final match = RegExp( + r'^HTTP/1[.][01] ([1-5][0-9]{2})(?: ([^\r\n]*))?$', + ).firstMatch(source.trim()); + if (match == null) { + throw _error( + DavErrorKind.malformedStatus, + 'DavMalformedHttpStatusLine', + 'The DAV server returned a malformed HTTP status line.', + correlationId, + ); + } + return ( + code: int.parse(match.group(1)!), + reason: match.group(2)?.trim() ?? '', + ); + } + + Set _parseErrorConditions(XmlElement container) { + final result = {}; + for (final error in _direct(container, davNamespace, 'error')) { + for (final child in error.childElements) { + result.add(DavPropertyName(child.name.namespaceUri, child.name.local)); + } + } + return Set.unmodifiable(result); + } + + void _enforceTreeLimits(XmlDocument document, String? correlationId) { + var elementCount = 0; + var textBytes = 0; + void visit(XmlNode node, int depth) { + if (depth > limits.maximumDepth) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlDepthLimitExceeded', + 'The DAV XML response exceeded the nesting limit.', + correlationId, + ); + } + if (node is XmlElement) { + elementCount += 1; + if (elementCount > limits.maximumElements) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlElementLimitExceeded', + 'The DAV XML response contained too many elements.', + correlationId, + ); + } + } else if (node is XmlText) { + textBytes += utf8.encode(node.value).length; + if (textBytes > limits.maximumTextBytes) { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlTextLimitExceeded', + 'The DAV XML response contained too much text.', + correlationId, + ); + } + } + for (final child in node.children) { + visit(child, depth + 1); + } + } + + visit(document, 0); + } + + String _decodeUtf8(Uint8List bytes, String? correlationId) { + try { + return utf8.decode(bytes, allowMalformed: false); + } on FormatException { + throw _error( + DavErrorKind.malformedXml, + 'DavXmlInvalidUtf8', + 'The DAV server returned XML that is not valid UTF-8.', + correlationId, + ); + } + } + + DavException _error( + DavErrorKind kind, + String code, + String safeMessage, + String? correlationId, + ) => DavException( + kind: kind, + code: code, + safeMessage: safeMessage, + correlationId: correlationId, + ); +} + +Iterable _direct( + XmlElement parent, + String namespaceUri, + String localName, +) => parent.childElements.where( + (element) => _matches(element, namespaceUri, localName), +); + +XmlElement? _firstDirect( + XmlElement parent, + String namespaceUri, + String localName, +) { + for (final element in parent.childElements) { + if (_matches(element, namespaceUri, localName)) { + return element; + } + } + return null; +} + +bool _matches(XmlElement element, String namespaceUri, String localName) => + element.name.namespaceUri == namespaceUri && + element.name.local == localName; diff --git a/lib/src/db/app_database.dart b/lib/src/db/app_database.dart index abce805..699efea 100644 --- a/lib/src/db/app_database.dart +++ b/lib/src/db/app_database.dart @@ -17,6 +17,11 @@ part 'daos/tasks_dao.dart'; @DriftDatabase( tables: [ Accounts, + DavAccountServices, + DavCollections, + DavObjects, + DavObjectComponents, + DavConflictSnapshots, TaskLists, Tasks, PendingOps, @@ -25,7 +30,7 @@ part 'daos/tasks_dao.dart'; CalendarEvents, CalendarEventAttendees, CalendarEventReminders, - CalendarSyncStates, + SyncCursors, CalendarColors, ScheduleItemOverrides, NotificationSchedule, diff --git a/lib/src/db/app_database.g.dart b/lib/src/db/app_database.g.dart index cdc1152..9ecf402 100644 --- a/lib/src/db/app_database.g.dart +++ b/lib/src/db/app_database.g.dart @@ -26,8 +26,18 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { aliasedName, false, type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('google'), + requiredDuringInsert: true, + ); + static const VerificationMeta _authorityMeta = const VerificationMeta( + 'authority', + ); + @override + late final GeneratedColumn authority = GeneratedColumn( + 'authority', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, ); static const VerificationMeta _providerAccountIdMeta = const VerificationMeta( 'providerAccountId', @@ -37,10 +47,32 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { GeneratedColumn( 'provider_account_id', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); + static const VerificationMeta _credentialKindMeta = const VerificationMeta( + 'credentialKind', + ); + @override + late final GeneratedColumn credentialKind = GeneratedColumn( + 'credential_kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _providerProfileVersionMeta = + const VerificationMeta('providerProfileVersion'); + @override + late final GeneratedColumn providerProfileVersion = GeneratedColumn( + 'provider_profile_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); static const VerificationMeta _displayNameMeta = const VerificationMeta( 'displayName', ); @@ -197,7 +229,10 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { List get $columns => [ id, provider, + authority, providerAccountId, + credentialKind, + providerProfileVersion, displayName, email, tenantId, @@ -234,6 +269,16 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { _providerMeta, provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('authority')) { + context.handle( + _authorityMeta, + authority.isAcceptableOrUnknown(data['authority']!, _authorityMeta), + ); + } else if (isInserting) { + context.missing(_authorityMeta); } if (data.containsKey('provider_account_id')) { context.handle( @@ -243,6 +288,28 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { _providerAccountIdMeta, ), ); + } else if (isInserting) { + context.missing(_providerAccountIdMeta); + } + if (data.containsKey('credential_kind')) { + context.handle( + _credentialKindMeta, + credentialKind.isAcceptableOrUnknown( + data['credential_kind']!, + _credentialKindMeta, + ), + ); + } else if (isInserting) { + context.missing(_credentialKindMeta); + } + if (data.containsKey('provider_profile_version')) { + context.handle( + _providerProfileVersionMeta, + providerProfileVersion.isAcceptableOrUnknown( + data['provider_profile_version']!, + _providerProfileVersionMeta, + ), + ); } if (data.containsKey('display_name')) { context.handle( @@ -373,10 +440,22 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { DriftSqlType.string, data['${effectivePrefix}provider'], )!, + authority: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}authority'], + )!, providerAccountId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}provider_account_id'], - ), + )!, + credentialKind: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}credential_kind'], + )!, + providerProfileVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}provider_profile_version'], + )!, displayName: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}display_name'], @@ -441,7 +520,10 @@ class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> { class Account extends DataClass implements Insertable { final String id; final String provider; - final String? providerAccountId; + final String authority; + final String providerAccountId; + final String credentialKind; + final int providerProfileVersion; final String? displayName; final String? email; final String? tenantId; @@ -458,7 +540,10 @@ class Account extends DataClass implements Insertable { const Account({ required this.id, required this.provider, - this.providerAccountId, + required this.authority, + required this.providerAccountId, + required this.credentialKind, + required this.providerProfileVersion, this.displayName, this.email, this.tenantId, @@ -478,9 +563,10 @@ class Account extends DataClass implements Insertable { final map = {}; map['id'] = Variable(id); map['provider'] = Variable(provider); - if (!nullToAbsent || providerAccountId != null) { - map['provider_account_id'] = Variable(providerAccountId); - } + map['authority'] = Variable(authority); + map['provider_account_id'] = Variable(providerAccountId); + map['credential_kind'] = Variable(credentialKind); + map['provider_profile_version'] = Variable(providerProfileVersion); if (!nullToAbsent || displayName != null) { map['display_name'] = Variable(displayName); } @@ -517,9 +603,10 @@ class Account extends DataClass implements Insertable { return AccountsCompanion( id: Value(id), provider: Value(provider), - providerAccountId: providerAccountId == null && nullToAbsent - ? const Value.absent() - : Value(providerAccountId), + authority: Value(authority), + providerAccountId: Value(providerAccountId), + credentialKind: Value(credentialKind), + providerProfileVersion: Value(providerProfileVersion), displayName: displayName == null && nullToAbsent ? const Value.absent() : Value(displayName), @@ -558,8 +645,11 @@ class Account extends DataClass implements Insertable { return Account( id: serializer.fromJson(json['id']), provider: serializer.fromJson(json['provider']), - providerAccountId: serializer.fromJson( - json['providerAccountId'], + authority: serializer.fromJson(json['authority']), + providerAccountId: serializer.fromJson(json['providerAccountId']), + credentialKind: serializer.fromJson(json['credentialKind']), + providerProfileVersion: serializer.fromJson( + json['providerProfileVersion'], ), displayName: serializer.fromJson(json['displayName']), email: serializer.fromJson(json['email']), @@ -588,7 +678,10 @@ class Account extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'provider': serializer.toJson(provider), - 'providerAccountId': serializer.toJson(providerAccountId), + 'authority': serializer.toJson(authority), + 'providerAccountId': serializer.toJson(providerAccountId), + 'credentialKind': serializer.toJson(credentialKind), + 'providerProfileVersion': serializer.toJson(providerProfileVersion), 'displayName': serializer.toJson(displayName), 'email': serializer.toJson(email), 'tenantId': serializer.toJson(tenantId), @@ -610,7 +703,10 @@ class Account extends DataClass implements Insertable { Account copyWith({ String? id, String? provider, - Value providerAccountId = const Value.absent(), + String? authority, + String? providerAccountId, + String? credentialKind, + int? providerProfileVersion, Value displayName = const Value.absent(), Value email = const Value.absent(), Value tenantId = const Value.absent(), @@ -627,9 +723,11 @@ class Account extends DataClass implements Insertable { }) => Account( id: id ?? this.id, provider: provider ?? this.provider, - providerAccountId: providerAccountId.present - ? providerAccountId.value - : this.providerAccountId, + authority: authority ?? this.authority, + providerAccountId: providerAccountId ?? this.providerAccountId, + credentialKind: credentialKind ?? this.credentialKind, + providerProfileVersion: + providerProfileVersion ?? this.providerProfileVersion, displayName: displayName.present ? displayName.value : this.displayName, email: email.present ? email.value : this.email, tenantId: tenantId.present ? tenantId.value : this.tenantId, @@ -656,9 +754,16 @@ class Account extends DataClass implements Insertable { return Account( id: data.id.present ? data.id.value : this.id, provider: data.provider.present ? data.provider.value : this.provider, + authority: data.authority.present ? data.authority.value : this.authority, providerAccountId: data.providerAccountId.present ? data.providerAccountId.value : this.providerAccountId, + credentialKind: data.credentialKind.present + ? data.credentialKind.value + : this.credentialKind, + providerProfileVersion: data.providerProfileVersion.present + ? data.providerProfileVersion.value + : this.providerProfileVersion, displayName: data.displayName.present ? data.displayName.value : this.displayName, @@ -700,7 +805,10 @@ class Account extends DataClass implements Insertable { return (StringBuffer('Account(') ..write('id: $id, ') ..write('provider: $provider, ') + ..write('authority: $authority, ') ..write('providerAccountId: $providerAccountId, ') + ..write('credentialKind: $credentialKind, ') + ..write('providerProfileVersion: $providerProfileVersion, ') ..write('displayName: $displayName, ') ..write('email: $email, ') ..write('tenantId: $tenantId, ') @@ -722,7 +830,10 @@ class Account extends DataClass implements Insertable { int get hashCode => Object.hash( id, provider, + authority, providerAccountId, + credentialKind, + providerProfileVersion, displayName, email, tenantId, @@ -743,7 +854,10 @@ class Account extends DataClass implements Insertable { (other is Account && other.id == this.id && other.provider == this.provider && + other.authority == this.authority && other.providerAccountId == this.providerAccountId && + other.credentialKind == this.credentialKind && + other.providerProfileVersion == this.providerProfileVersion && other.displayName == this.displayName && other.email == this.email && other.tenantId == this.tenantId && @@ -762,7 +876,10 @@ class Account extends DataClass implements Insertable { class AccountsCompanion extends UpdateCompanion { final Value id; final Value provider; - final Value providerAccountId; + final Value authority; + final Value providerAccountId; + final Value credentialKind; + final Value providerProfileVersion; final Value displayName; final Value email; final Value tenantId; @@ -780,7 +897,10 @@ class AccountsCompanion extends UpdateCompanion { const AccountsCompanion({ this.id = const Value.absent(), this.provider = const Value.absent(), + this.authority = const Value.absent(), this.providerAccountId = const Value.absent(), + this.credentialKind = const Value.absent(), + this.providerProfileVersion = const Value.absent(), this.displayName = const Value.absent(), this.email = const Value.absent(), this.tenantId = const Value.absent(), @@ -798,8 +918,11 @@ class AccountsCompanion extends UpdateCompanion { }); AccountsCompanion.insert({ required String id, - this.provider = const Value.absent(), - this.providerAccountId = const Value.absent(), + required String provider, + required String authority, + required String providerAccountId, + required String credentialKind, + this.providerProfileVersion = const Value.absent(), this.displayName = const Value.absent(), this.email = const Value.absent(), this.tenantId = const Value.absent(), @@ -815,12 +938,19 @@ class AccountsCompanion extends UpdateCompanion { this.lastFullSyncAtUtc = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), + provider = Value(provider), + authority = Value(authority), + providerAccountId = Value(providerAccountId), + credentialKind = Value(credentialKind), createdAtUtc = Value(createdAtUtc), updatedAtUtc = Value(updatedAtUtc); static Insertable custom({ Expression? id, Expression? provider, + Expression? authority, Expression? providerAccountId, + Expression? credentialKind, + Expression? providerProfileVersion, Expression? displayName, Expression? email, Expression? tenantId, @@ -839,7 +969,11 @@ class AccountsCompanion extends UpdateCompanion { return RawValuesInsertable({ if (id != null) 'id': id, if (provider != null) 'provider': provider, + if (authority != null) 'authority': authority, if (providerAccountId != null) 'provider_account_id': providerAccountId, + if (credentialKind != null) 'credential_kind': credentialKind, + if (providerProfileVersion != null) + 'provider_profile_version': providerProfileVersion, if (displayName != null) 'display_name': displayName, if (email != null) 'email': email, if (tenantId != null) 'tenant_id': tenantId, @@ -862,7 +996,10 @@ class AccountsCompanion extends UpdateCompanion { AccountsCompanion copyWith({ Value? id, Value? provider, - Value? providerAccountId, + Value? authority, + Value? providerAccountId, + Value? credentialKind, + Value? providerProfileVersion, Value? displayName, Value? email, Value? tenantId, @@ -881,7 +1018,11 @@ class AccountsCompanion extends UpdateCompanion { return AccountsCompanion( id: id ?? this.id, provider: provider ?? this.provider, + authority: authority ?? this.authority, providerAccountId: providerAccountId ?? this.providerAccountId, + credentialKind: credentialKind ?? this.credentialKind, + providerProfileVersion: + providerProfileVersion ?? this.providerProfileVersion, displayName: displayName ?? this.displayName, email: email ?? this.email, tenantId: tenantId ?? this.tenantId, @@ -909,9 +1050,20 @@ class AccountsCompanion extends UpdateCompanion { if (provider.present) { map['provider'] = Variable(provider.value); } + if (authority.present) { + map['authority'] = Variable(authority.value); + } if (providerAccountId.present) { map['provider_account_id'] = Variable(providerAccountId.value); } + if (credentialKind.present) { + map['credential_kind'] = Variable(credentialKind.value); + } + if (providerProfileVersion.present) { + map['provider_profile_version'] = Variable( + providerProfileVersion.value, + ); + } if (displayName.present) { map['display_name'] = Variable(displayName.value); } @@ -966,7 +1118,10 @@ class AccountsCompanion extends UpdateCompanion { return (StringBuffer('AccountsCompanion(') ..write('id: $id, ') ..write('provider: $provider, ') + ..write('authority: $authority, ') ..write('providerAccountId: $providerAccountId, ') + ..write('credentialKind: $credentialKind, ') + ..write('providerProfileVersion: $providerProfileVersion, ') ..write('displayName: $displayName, ') ..write('email: $email, ') ..write('tenantId: $tenantId, ') @@ -986,12 +1141,12 @@ class AccountsCompanion extends UpdateCompanion { } } -class $TaskListsTable extends TaskLists - with TableInfo<$TaskListsTable, TaskList> { +class $DavAccountServicesTable extends DavAccountServices + with TableInfo<$DavAccountServicesTable, DavAccountService> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $TaskListsTable(this.attachedDatabase, [this._alias]); + $DavAccountServicesTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _accountIdMeta = const VerificationMeta( 'accountId', ); @@ -1006,246 +1161,178 @@ class $TaskListsTable extends TaskLists 'REFERENCES accounts (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _kindMeta = const VerificationMeta('kind'); - @override - late final GeneratedColumn kind = GeneratedColumn( - 'kind', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _etagMeta = const VerificationMeta('etag'); + static const VerificationMeta _canonicalServiceUriMeta = + const VerificationMeta('canonicalServiceUri'); @override - late final GeneratedColumn etag = GeneratedColumn( - 'etag', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, + late final GeneratedColumn canonicalServiceUri = + GeneratedColumn( + 'canonical_service_uri', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _canonicalOriginMeta = const VerificationMeta( + 'canonicalOrigin', ); - static const VerificationMeta _titleMeta = const VerificationMeta('title'); @override - late final GeneratedColumn title = GeneratedColumn( - 'title', + late final GeneratedColumn canonicalOrigin = GeneratedColumn( + 'canonical_origin', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _updatedUtcMeta = const VerificationMeta( - 'updatedUtc', - ); - @override - late final GeneratedColumn updatedUtc = GeneratedColumn( - 'updated_utc', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _selfLinkMeta = const VerificationMeta( - 'selfLink', + static const VerificationMeta _principalHrefMeta = const VerificationMeta( + 'principalHref', ); @override - late final GeneratedColumn selfLink = GeneratedColumn( - 'self_link', + late final GeneratedColumn principalHref = GeneratedColumn( + 'principal_href', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', - ); - @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _providerListKindMeta = const VerificationMeta( - 'providerListKind', + static const VerificationMeta _calendarHomeHrefMeta = const VerificationMeta( + 'calendarHomeHref', ); @override - late final GeneratedColumn providerListKind = GeneratedColumn( - 'provider_list_kind', + late final GeneratedColumn calendarHomeHref = GeneratedColumn( + 'calendar_home_href', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _isOwnerMeta = const VerificationMeta( - 'isOwner', - ); - @override - late final GeneratedColumn isOwner = GeneratedColumn( - 'is_owner', - aliasedName, - true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_owner" IN (0, 1))', - ), - ); - static const VerificationMeta _isSharedMeta = const VerificationMeta( - 'isShared', - ); + static const VerificationMeta _calendarUserAddressesJsonMeta = + const VerificationMeta('calendarUserAddressesJson'); @override - late final GeneratedColumn isShared = GeneratedColumn( - 'is_shared', - aliasedName, - true, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_shared" IN (0, 1))', - ), - ); - static const VerificationMeta _deltaLinkMeta = const VerificationMeta( - 'deltaLink', + late final GeneratedColumn calendarUserAddressesJson = + GeneratedColumn( + 'calendar_user_addresses_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('[]'), + ); + static const VerificationMeta _scheduleInboxHrefMeta = const VerificationMeta( + 'scheduleInboxHref', ); @override - late final GeneratedColumn deltaLink = GeneratedColumn( - 'delta_link', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _providerMetadataJsonMeta = - const VerificationMeta('providerMetadataJson'); + late final GeneratedColumn scheduleInboxHref = + GeneratedColumn( + 'schedule_inbox_href', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _scheduleOutboxHrefMeta = + const VerificationMeta('scheduleOutboxHref'); @override - late final GeneratedColumn providerMetadataJson = + late final GeneratedColumn scheduleOutboxHref = GeneratedColumn( - 'provider_metadata_json', + 'schedule_outbox_href', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _serverMissingMeta = const VerificationMeta( - 'serverMissing', + static const VerificationMeta _capabilitiesJsonMeta = const VerificationMeta( + 'capabilitiesJson', ); @override - late final GeneratedColumn serverMissing = GeneratedColumn( - 'server_missing', + late final GeneratedColumn capabilitiesJson = GeneratedColumn( + 'capabilities_json', aliasedName, false, - type: DriftSqlType.bool, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("server_missing" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _localDirtyMeta = const VerificationMeta( - 'localDirty', + defaultValue: const Constant('{}'), ); + static const VerificationMeta _capabilitiesSchemaVersionMeta = + const VerificationMeta('capabilitiesSchemaVersion'); @override - late final GeneratedColumn localDirty = GeneratedColumn( - 'local_dirty', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("local_dirty" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _pendingDeleteMeta = const VerificationMeta( - 'pendingDelete', - ); + late final GeneratedColumn capabilitiesSchemaVersion = + GeneratedColumn( + 'capabilities_schema_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); + static const VerificationMeta _providerProfileVersionMeta = + const VerificationMeta('providerProfileVersion'); @override - late final GeneratedColumn pendingDelete = GeneratedColumn( - 'pending_delete', + late final GeneratedColumn providerProfileVersion = GeneratedColumn( + 'provider_profile_version', aliasedName, false, - type: DriftSqlType.bool, + type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("pending_delete" IN (0, 1))', - ), - defaultValue: const Constant(false), + defaultValue: const Constant(1), ); - static const VerificationMeta _lastSyncedAtUtcMeta = const VerificationMeta( - 'lastSyncedAtUtc', + static const VerificationMeta _discoveredAtUtcMeta = const VerificationMeta( + 'discoveredAtUtc', ); @override - late final GeneratedColumn lastSyncedAtUtc = GeneratedColumn( - 'last_synced_at_utc', + late final GeneratedColumn discoveredAtUtc = GeneratedColumn( + 'discovered_at_utc', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _createdLocalAtUtcMeta = const VerificationMeta( - 'createdLocalAtUtc', + requiredDuringInsert: true, ); + static const VerificationMeta _lastValidatedAtUtcMeta = + const VerificationMeta('lastValidatedAtUtc'); @override - late final GeneratedColumn createdLocalAtUtc = + late final GeneratedColumn lastValidatedAtUtc = GeneratedColumn( - 'created_local_at_utc', + 'last_validated_at_utc', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, + requiredDuringInsert: false, ); - static const VerificationMeta _updatedLocalAtUtcMeta = const VerificationMeta( - 'updatedLocalAtUtc', - ); + static const VerificationMeta _lastDiscoveryErrorCodeMeta = + const VerificationMeta('lastDiscoveryErrorCode'); @override - late final GeneratedColumn updatedLocalAtUtc = + late final GeneratedColumn lastDiscoveryErrorCode = GeneratedColumn( - 'updated_local_at_utc', + 'last_discovery_error_code', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, + requiredDuringInsert: false, ); @override List get $columns => [ accountId, - id, - kind, - etag, - title, - updatedUtc, - selfLink, - rawJson, - providerListKind, - isOwner, - isShared, - deltaLink, - providerMetadataJson, - serverMissing, - localDirty, - pendingDelete, - lastSyncedAtUtc, - createdLocalAtUtc, - updatedLocalAtUtc, + canonicalServiceUri, + canonicalOrigin, + principalHref, + calendarHomeHref, + calendarUserAddressesJson, + scheduleInboxHref, + scheduleOutboxHref, + capabilitiesJson, + capabilitiesSchemaVersion, + providerProfileVersion, + discoveredAtUtc, + lastValidatedAtUtc, + lastDiscoveryErrorCode, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'task_lists'; + static const String $name = 'dav_account_services'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); @@ -1258,389 +1345,340 @@ class $TaskListsTable extends TaskLists } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } - if (data.containsKey('kind')) { - context.handle( - _kindMeta, - kind.isAcceptableOrUnknown(data['kind']!, _kindMeta), - ); - } - if (data.containsKey('etag')) { - context.handle( - _etagMeta, - etag.isAcceptableOrUnknown(data['etag']!, _etagMeta), - ); - } - if (data.containsKey('title')) { + if (data.containsKey('canonical_service_uri')) { context.handle( - _titleMeta, - title.isAcceptableOrUnknown(data['title']!, _titleMeta), + _canonicalServiceUriMeta, + canonicalServiceUri.isAcceptableOrUnknown( + data['canonical_service_uri']!, + _canonicalServiceUriMeta, + ), ); } else if (isInserting) { - context.missing(_titleMeta); - } - if (data.containsKey('updated_utc')) { - context.handle( - _updatedUtcMeta, - updatedUtc.isAcceptableOrUnknown(data['updated_utc']!, _updatedUtcMeta), - ); - } - if (data.containsKey('self_link')) { - context.handle( - _selfLinkMeta, - selfLink.isAcceptableOrUnknown(data['self_link']!, _selfLinkMeta), - ); + context.missing(_canonicalServiceUriMeta); } - if (data.containsKey('raw_json')) { + if (data.containsKey('canonical_origin')) { context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + _canonicalOriginMeta, + canonicalOrigin.isAcceptableOrUnknown( + data['canonical_origin']!, + _canonicalOriginMeta, + ), ); } else if (isInserting) { - context.missing(_rawJsonMeta); + context.missing(_canonicalOriginMeta); } - if (data.containsKey('provider_list_kind')) { + if (data.containsKey('principal_href')) { context.handle( - _providerListKindMeta, - providerListKind.isAcceptableOrUnknown( - data['provider_list_kind']!, - _providerListKindMeta, + _principalHrefMeta, + principalHref.isAcceptableOrUnknown( + data['principal_href']!, + _principalHrefMeta, ), ); } - if (data.containsKey('is_owner')) { + if (data.containsKey('calendar_home_href')) { context.handle( - _isOwnerMeta, - isOwner.isAcceptableOrUnknown(data['is_owner']!, _isOwnerMeta), + _calendarHomeHrefMeta, + calendarHomeHref.isAcceptableOrUnknown( + data['calendar_home_href']!, + _calendarHomeHrefMeta, + ), ); } - if (data.containsKey('is_shared')) { + if (data.containsKey('calendar_user_addresses_json')) { context.handle( - _isSharedMeta, - isShared.isAcceptableOrUnknown(data['is_shared']!, _isSharedMeta), + _calendarUserAddressesJsonMeta, + calendarUserAddressesJson.isAcceptableOrUnknown( + data['calendar_user_addresses_json']!, + _calendarUserAddressesJsonMeta, + ), ); } - if (data.containsKey('delta_link')) { + if (data.containsKey('schedule_inbox_href')) { context.handle( - _deltaLinkMeta, - deltaLink.isAcceptableOrUnknown(data['delta_link']!, _deltaLinkMeta), + _scheduleInboxHrefMeta, + scheduleInboxHref.isAcceptableOrUnknown( + data['schedule_inbox_href']!, + _scheduleInboxHrefMeta, + ), ); } - if (data.containsKey('provider_metadata_json')) { + if (data.containsKey('schedule_outbox_href')) { context.handle( - _providerMetadataJsonMeta, - providerMetadataJson.isAcceptableOrUnknown( - data['provider_metadata_json']!, - _providerMetadataJsonMeta, + _scheduleOutboxHrefMeta, + scheduleOutboxHref.isAcceptableOrUnknown( + data['schedule_outbox_href']!, + _scheduleOutboxHrefMeta, ), ); } - if (data.containsKey('server_missing')) { + if (data.containsKey('capabilities_json')) { context.handle( - _serverMissingMeta, - serverMissing.isAcceptableOrUnknown( - data['server_missing']!, - _serverMissingMeta, + _capabilitiesJsonMeta, + capabilitiesJson.isAcceptableOrUnknown( + data['capabilities_json']!, + _capabilitiesJsonMeta, ), ); } - if (data.containsKey('local_dirty')) { + if (data.containsKey('capabilities_schema_version')) { context.handle( - _localDirtyMeta, - localDirty.isAcceptableOrUnknown(data['local_dirty']!, _localDirtyMeta), + _capabilitiesSchemaVersionMeta, + capabilitiesSchemaVersion.isAcceptableOrUnknown( + data['capabilities_schema_version']!, + _capabilitiesSchemaVersionMeta, + ), ); } - if (data.containsKey('pending_delete')) { + if (data.containsKey('provider_profile_version')) { context.handle( - _pendingDeleteMeta, - pendingDelete.isAcceptableOrUnknown( - data['pending_delete']!, - _pendingDeleteMeta, + _providerProfileVersionMeta, + providerProfileVersion.isAcceptableOrUnknown( + data['provider_profile_version']!, + _providerProfileVersionMeta, ), ); } - if (data.containsKey('last_synced_at_utc')) { + if (data.containsKey('discovered_at_utc')) { context.handle( - _lastSyncedAtUtcMeta, - lastSyncedAtUtc.isAcceptableOrUnknown( - data['last_synced_at_utc']!, - _lastSyncedAtUtcMeta, + _discoveredAtUtcMeta, + discoveredAtUtc.isAcceptableOrUnknown( + data['discovered_at_utc']!, + _discoveredAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_discoveredAtUtcMeta); } - if (data.containsKey('created_local_at_utc')) { + if (data.containsKey('last_validated_at_utc')) { context.handle( - _createdLocalAtUtcMeta, - createdLocalAtUtc.isAcceptableOrUnknown( - data['created_local_at_utc']!, - _createdLocalAtUtcMeta, + _lastValidatedAtUtcMeta, + lastValidatedAtUtc.isAcceptableOrUnknown( + data['last_validated_at_utc']!, + _lastValidatedAtUtcMeta, ), ); - } else if (isInserting) { - context.missing(_createdLocalAtUtcMeta); } - if (data.containsKey('updated_local_at_utc')) { + if (data.containsKey('last_discovery_error_code')) { context.handle( - _updatedLocalAtUtcMeta, - updatedLocalAtUtc.isAcceptableOrUnknown( - data['updated_local_at_utc']!, - _updatedLocalAtUtcMeta, + _lastDiscoveryErrorCodeMeta, + lastDiscoveryErrorCode.isAcceptableOrUnknown( + data['last_discovery_error_code']!, + _lastDiscoveryErrorCodeMeta, ), ); - } else if (isInserting) { - context.missing(_updatedLocalAtUtcMeta); } return context; } @override - Set get $primaryKey => {accountId, id}; + Set get $primaryKey => {accountId}; @override - TaskList map(Map data, {String? tablePrefix}) { + DavAccountService map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return TaskList( + return DavAccountService( accountId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - id: attachedDatabase.typeMapping.read( + canonicalServiceUri: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}id'], + data['${effectivePrefix}canonical_service_uri'], )!, - kind: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}kind'], - ), - etag: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}etag'], - ), - title: attachedDatabase.typeMapping.read( + canonicalOrigin: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}title'], + data['${effectivePrefix}canonical_origin'], )!, - updatedUtc: attachedDatabase.typeMapping.read( + principalHref: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_utc'], + data['${effectivePrefix}principal_href'], ), - selfLink: attachedDatabase.typeMapping.read( + calendarHomeHref: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}self_link'], + data['${effectivePrefix}calendar_home_href'], ), - rawJson: attachedDatabase.typeMapping.read( + calendarUserAddressesJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}raw_json'], + data['${effectivePrefix}calendar_user_addresses_json'], )!, - providerListKind: attachedDatabase.typeMapping.read( + scheduleInboxHref: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_list_kind'], - ), - isOwner: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}is_owner'], - ), - isShared: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}is_shared'], + data['${effectivePrefix}schedule_inbox_href'], ), - deltaLink: attachedDatabase.typeMapping.read( + scheduleOutboxHref: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}delta_link'], + data['${effectivePrefix}schedule_outbox_href'], ), - providerMetadataJson: attachedDatabase.typeMapping.read( + capabilitiesJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_metadata_json'], - ), - serverMissing: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}server_missing'], + data['${effectivePrefix}capabilities_json'], )!, - localDirty: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}local_dirty'], + capabilitiesSchemaVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}capabilities_schema_version'], )!, - pendingDelete: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}pending_delete'], + providerProfileVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}provider_profile_version'], )!, - lastSyncedAtUtc: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}last_synced_at_utc'], - ), - createdLocalAtUtc: attachedDatabase.typeMapping.read( + discoveredAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}created_local_at_utc'], + data['${effectivePrefix}discovered_at_utc'], )!, - updatedLocalAtUtc: attachedDatabase.typeMapping.read( + lastValidatedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_local_at_utc'], - )!, + data['${effectivePrefix}last_validated_at_utc'], + ), + lastDiscoveryErrorCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_discovery_error_code'], + ), ); } @override - $TaskListsTable createAlias(String alias) { - return $TaskListsTable(attachedDatabase, alias); + $DavAccountServicesTable createAlias(String alias) { + return $DavAccountServicesTable(attachedDatabase, alias); } } -class TaskList extends DataClass implements Insertable { +class DavAccountService extends DataClass + implements Insertable { final String accountId; - final String id; - final String? kind; - final String? etag; - final String title; - final String? updatedUtc; - final String? selfLink; - final String rawJson; - final String? providerListKind; - final bool? isOwner; - final bool? isShared; - final String? deltaLink; - final String? providerMetadataJson; - final bool serverMissing; - final bool localDirty; - final bool pendingDelete; - final String? lastSyncedAtUtc; - final String createdLocalAtUtc; - final String updatedLocalAtUtc; - const TaskList({ + final String canonicalServiceUri; + final String canonicalOrigin; + final String? principalHref; + final String? calendarHomeHref; + final String calendarUserAddressesJson; + final String? scheduleInboxHref; + final String? scheduleOutboxHref; + final String capabilitiesJson; + final int capabilitiesSchemaVersion; + final int providerProfileVersion; + final String discoveredAtUtc; + final String? lastValidatedAtUtc; + final String? lastDiscoveryErrorCode; + const DavAccountService({ required this.accountId, - required this.id, - this.kind, - this.etag, - required this.title, - this.updatedUtc, - this.selfLink, - required this.rawJson, - this.providerListKind, - this.isOwner, - this.isShared, - this.deltaLink, - this.providerMetadataJson, - required this.serverMissing, - required this.localDirty, - required this.pendingDelete, - this.lastSyncedAtUtc, - required this.createdLocalAtUtc, - required this.updatedLocalAtUtc, + required this.canonicalServiceUri, + required this.canonicalOrigin, + this.principalHref, + this.calendarHomeHref, + required this.calendarUserAddressesJson, + this.scheduleInboxHref, + this.scheduleOutboxHref, + required this.capabilitiesJson, + required this.capabilitiesSchemaVersion, + required this.providerProfileVersion, + required this.discoveredAtUtc, + this.lastValidatedAtUtc, + this.lastDiscoveryErrorCode, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['account_id'] = Variable(accountId); - map['id'] = Variable(id); - if (!nullToAbsent || kind != null) { - map['kind'] = Variable(kind); - } - if (!nullToAbsent || etag != null) { - map['etag'] = Variable(etag); + map['canonical_service_uri'] = Variable(canonicalServiceUri); + map['canonical_origin'] = Variable(canonicalOrigin); + if (!nullToAbsent || principalHref != null) { + map['principal_href'] = Variable(principalHref); } - map['title'] = Variable(title); - if (!nullToAbsent || updatedUtc != null) { - map['updated_utc'] = Variable(updatedUtc); - } - if (!nullToAbsent || selfLink != null) { - map['self_link'] = Variable(selfLink); - } - map['raw_json'] = Variable(rawJson); - if (!nullToAbsent || providerListKind != null) { - map['provider_list_kind'] = Variable(providerListKind); - } - if (!nullToAbsent || isOwner != null) { - map['is_owner'] = Variable(isOwner); + if (!nullToAbsent || calendarHomeHref != null) { + map['calendar_home_href'] = Variable(calendarHomeHref); } - if (!nullToAbsent || isShared != null) { - map['is_shared'] = Variable(isShared); + map['calendar_user_addresses_json'] = Variable( + calendarUserAddressesJson, + ); + if (!nullToAbsent || scheduleInboxHref != null) { + map['schedule_inbox_href'] = Variable(scheduleInboxHref); } - if (!nullToAbsent || deltaLink != null) { - map['delta_link'] = Variable(deltaLink); + if (!nullToAbsent || scheduleOutboxHref != null) { + map['schedule_outbox_href'] = Variable(scheduleOutboxHref); } - if (!nullToAbsent || providerMetadataJson != null) { - map['provider_metadata_json'] = Variable(providerMetadataJson); + map['capabilities_json'] = Variable(capabilitiesJson); + map['capabilities_schema_version'] = Variable( + capabilitiesSchemaVersion, + ); + map['provider_profile_version'] = Variable(providerProfileVersion); + map['discovered_at_utc'] = Variable(discoveredAtUtc); + if (!nullToAbsent || lastValidatedAtUtc != null) { + map['last_validated_at_utc'] = Variable(lastValidatedAtUtc); } - map['server_missing'] = Variable(serverMissing); - map['local_dirty'] = Variable(localDirty); - map['pending_delete'] = Variable(pendingDelete); - if (!nullToAbsent || lastSyncedAtUtc != null) { - map['last_synced_at_utc'] = Variable(lastSyncedAtUtc); + if (!nullToAbsent || lastDiscoveryErrorCode != null) { + map['last_discovery_error_code'] = Variable( + lastDiscoveryErrorCode, + ); } - map['created_local_at_utc'] = Variable(createdLocalAtUtc); - map['updated_local_at_utc'] = Variable(updatedLocalAtUtc); return map; } - TaskListsCompanion toCompanion(bool nullToAbsent) { - return TaskListsCompanion( + DavAccountServicesCompanion toCompanion(bool nullToAbsent) { + return DavAccountServicesCompanion( accountId: Value(accountId), - id: Value(id), - kind: kind == null && nullToAbsent ? const Value.absent() : Value(kind), - etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag), - title: Value(title), - updatedUtc: updatedUtc == null && nullToAbsent + canonicalServiceUri: Value(canonicalServiceUri), + canonicalOrigin: Value(canonicalOrigin), + principalHref: principalHref == null && nullToAbsent ? const Value.absent() - : Value(updatedUtc), - selfLink: selfLink == null && nullToAbsent + : Value(principalHref), + calendarHomeHref: calendarHomeHref == null && nullToAbsent ? const Value.absent() - : Value(selfLink), - rawJson: Value(rawJson), - providerListKind: providerListKind == null && nullToAbsent + : Value(calendarHomeHref), + calendarUserAddressesJson: Value(calendarUserAddressesJson), + scheduleInboxHref: scheduleInboxHref == null && nullToAbsent ? const Value.absent() - : Value(providerListKind), - isOwner: isOwner == null && nullToAbsent + : Value(scheduleInboxHref), + scheduleOutboxHref: scheduleOutboxHref == null && nullToAbsent ? const Value.absent() - : Value(isOwner), - isShared: isShared == null && nullToAbsent + : Value(scheduleOutboxHref), + capabilitiesJson: Value(capabilitiesJson), + capabilitiesSchemaVersion: Value(capabilitiesSchemaVersion), + providerProfileVersion: Value(providerProfileVersion), + discoveredAtUtc: Value(discoveredAtUtc), + lastValidatedAtUtc: lastValidatedAtUtc == null && nullToAbsent ? const Value.absent() - : Value(isShared), - deltaLink: deltaLink == null && nullToAbsent + : Value(lastValidatedAtUtc), + lastDiscoveryErrorCode: lastDiscoveryErrorCode == null && nullToAbsent ? const Value.absent() - : Value(deltaLink), - providerMetadataJson: providerMetadataJson == null && nullToAbsent - ? const Value.absent() - : Value(providerMetadataJson), - serverMissing: Value(serverMissing), - localDirty: Value(localDirty), - pendingDelete: Value(pendingDelete), - lastSyncedAtUtc: lastSyncedAtUtc == null && nullToAbsent - ? const Value.absent() - : Value(lastSyncedAtUtc), - createdLocalAtUtc: Value(createdLocalAtUtc), - updatedLocalAtUtc: Value(updatedLocalAtUtc), + : Value(lastDiscoveryErrorCode), ); } - factory TaskList.fromJson( + factory DavAccountService.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return TaskList( + return DavAccountService( accountId: serializer.fromJson(json['accountId']), - id: serializer.fromJson(json['id']), - kind: serializer.fromJson(json['kind']), - etag: serializer.fromJson(json['etag']), - title: serializer.fromJson(json['title']), - updatedUtc: serializer.fromJson(json['updatedUtc']), - selfLink: serializer.fromJson(json['selfLink']), - rawJson: serializer.fromJson(json['rawJson']), - providerListKind: serializer.fromJson(json['providerListKind']), - isOwner: serializer.fromJson(json['isOwner']), - isShared: serializer.fromJson(json['isShared']), - deltaLink: serializer.fromJson(json['deltaLink']), - providerMetadataJson: serializer.fromJson( - json['providerMetadataJson'], + canonicalServiceUri: serializer.fromJson( + json['canonicalServiceUri'], + ), + canonicalOrigin: serializer.fromJson(json['canonicalOrigin']), + principalHref: serializer.fromJson(json['principalHref']), + calendarHomeHref: serializer.fromJson(json['calendarHomeHref']), + calendarUserAddressesJson: serializer.fromJson( + json['calendarUserAddressesJson'], + ), + scheduleInboxHref: serializer.fromJson( + json['scheduleInboxHref'], + ), + scheduleOutboxHref: serializer.fromJson( + json['scheduleOutboxHref'], + ), + capabilitiesJson: serializer.fromJson(json['capabilitiesJson']), + capabilitiesSchemaVersion: serializer.fromJson( + json['capabilitiesSchemaVersion'], + ), + providerProfileVersion: serializer.fromJson( + json['providerProfileVersion'], + ), + discoveredAtUtc: serializer.fromJson(json['discoveredAtUtc']), + lastValidatedAtUtc: serializer.fromJson( + json['lastValidatedAtUtc'], + ), + lastDiscoveryErrorCode: serializer.fromJson( + json['lastDiscoveryErrorCode'], ), - serverMissing: serializer.fromJson(json['serverMissing']), - localDirty: serializer.fromJson(json['localDirty']), - pendingDelete: serializer.fromJson(json['pendingDelete']), - lastSyncedAtUtc: serializer.fromJson(json['lastSyncedAtUtc']), - createdLocalAtUtc: serializer.fromJson(json['createdLocalAtUtc']), - updatedLocalAtUtc: serializer.fromJson(json['updatedLocalAtUtc']), ); } @override @@ -1648,138 +1686,136 @@ class TaskList extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'accountId': serializer.toJson(accountId), - 'id': serializer.toJson(id), - 'kind': serializer.toJson(kind), - 'etag': serializer.toJson(etag), - 'title': serializer.toJson(title), - 'updatedUtc': serializer.toJson(updatedUtc), - 'selfLink': serializer.toJson(selfLink), - 'rawJson': serializer.toJson(rawJson), - 'providerListKind': serializer.toJson(providerListKind), - 'isOwner': serializer.toJson(isOwner), - 'isShared': serializer.toJson(isShared), - 'deltaLink': serializer.toJson(deltaLink), - 'providerMetadataJson': serializer.toJson(providerMetadataJson), - 'serverMissing': serializer.toJson(serverMissing), - 'localDirty': serializer.toJson(localDirty), - 'pendingDelete': serializer.toJson(pendingDelete), - 'lastSyncedAtUtc': serializer.toJson(lastSyncedAtUtc), - 'createdLocalAtUtc': serializer.toJson(createdLocalAtUtc), - 'updatedLocalAtUtc': serializer.toJson(updatedLocalAtUtc), + 'canonicalServiceUri': serializer.toJson(canonicalServiceUri), + 'canonicalOrigin': serializer.toJson(canonicalOrigin), + 'principalHref': serializer.toJson(principalHref), + 'calendarHomeHref': serializer.toJson(calendarHomeHref), + 'calendarUserAddressesJson': serializer.toJson( + calendarUserAddressesJson, + ), + 'scheduleInboxHref': serializer.toJson(scheduleInboxHref), + 'scheduleOutboxHref': serializer.toJson(scheduleOutboxHref), + 'capabilitiesJson': serializer.toJson(capabilitiesJson), + 'capabilitiesSchemaVersion': serializer.toJson( + capabilitiesSchemaVersion, + ), + 'providerProfileVersion': serializer.toJson(providerProfileVersion), + 'discoveredAtUtc': serializer.toJson(discoveredAtUtc), + 'lastValidatedAtUtc': serializer.toJson(lastValidatedAtUtc), + 'lastDiscoveryErrorCode': serializer.toJson( + lastDiscoveryErrorCode, + ), }; } - TaskList copyWith({ + DavAccountService copyWith({ String? accountId, - String? id, - Value kind = const Value.absent(), - Value etag = const Value.absent(), - String? title, - Value updatedUtc = const Value.absent(), - Value selfLink = const Value.absent(), - String? rawJson, - Value providerListKind = const Value.absent(), - Value isOwner = const Value.absent(), - Value isShared = const Value.absent(), - Value deltaLink = const Value.absent(), - Value providerMetadataJson = const Value.absent(), - bool? serverMissing, - bool? localDirty, - bool? pendingDelete, - Value lastSyncedAtUtc = const Value.absent(), - String? createdLocalAtUtc, - String? updatedLocalAtUtc, - }) => TaskList( + String? canonicalServiceUri, + String? canonicalOrigin, + Value principalHref = const Value.absent(), + Value calendarHomeHref = const Value.absent(), + String? calendarUserAddressesJson, + Value scheduleInboxHref = const Value.absent(), + Value scheduleOutboxHref = const Value.absent(), + String? capabilitiesJson, + int? capabilitiesSchemaVersion, + int? providerProfileVersion, + String? discoveredAtUtc, + Value lastValidatedAtUtc = const Value.absent(), + Value lastDiscoveryErrorCode = const Value.absent(), + }) => DavAccountService( accountId: accountId ?? this.accountId, - id: id ?? this.id, - kind: kind.present ? kind.value : this.kind, - etag: etag.present ? etag.value : this.etag, - title: title ?? this.title, - updatedUtc: updatedUtc.present ? updatedUtc.value : this.updatedUtc, - selfLink: selfLink.present ? selfLink.value : this.selfLink, - rawJson: rawJson ?? this.rawJson, - providerListKind: providerListKind.present - ? providerListKind.value - : this.providerListKind, - isOwner: isOwner.present ? isOwner.value : this.isOwner, - isShared: isShared.present ? isShared.value : this.isShared, - deltaLink: deltaLink.present ? deltaLink.value : this.deltaLink, - providerMetadataJson: providerMetadataJson.present - ? providerMetadataJson.value - : this.providerMetadataJson, - serverMissing: serverMissing ?? this.serverMissing, - localDirty: localDirty ?? this.localDirty, - pendingDelete: pendingDelete ?? this.pendingDelete, - lastSyncedAtUtc: lastSyncedAtUtc.present - ? lastSyncedAtUtc.value - : this.lastSyncedAtUtc, - createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, - updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, - ); - TaskList copyWithCompanion(TaskListsCompanion data) { - return TaskList( + canonicalServiceUri: canonicalServiceUri ?? this.canonicalServiceUri, + canonicalOrigin: canonicalOrigin ?? this.canonicalOrigin, + principalHref: principalHref.present + ? principalHref.value + : this.principalHref, + calendarHomeHref: calendarHomeHref.present + ? calendarHomeHref.value + : this.calendarHomeHref, + calendarUserAddressesJson: + calendarUserAddressesJson ?? this.calendarUserAddressesJson, + scheduleInboxHref: scheduleInboxHref.present + ? scheduleInboxHref.value + : this.scheduleInboxHref, + scheduleOutboxHref: scheduleOutboxHref.present + ? scheduleOutboxHref.value + : this.scheduleOutboxHref, + capabilitiesJson: capabilitiesJson ?? this.capabilitiesJson, + capabilitiesSchemaVersion: + capabilitiesSchemaVersion ?? this.capabilitiesSchemaVersion, + providerProfileVersion: + providerProfileVersion ?? this.providerProfileVersion, + discoveredAtUtc: discoveredAtUtc ?? this.discoveredAtUtc, + lastValidatedAtUtc: lastValidatedAtUtc.present + ? lastValidatedAtUtc.value + : this.lastValidatedAtUtc, + lastDiscoveryErrorCode: lastDiscoveryErrorCode.present + ? lastDiscoveryErrorCode.value + : this.lastDiscoveryErrorCode, + ); + DavAccountService copyWithCompanion(DavAccountServicesCompanion data) { + return DavAccountService( accountId: data.accountId.present ? data.accountId.value : this.accountId, - id: data.id.present ? data.id.value : this.id, - kind: data.kind.present ? data.kind.value : this.kind, - etag: data.etag.present ? data.etag.value : this.etag, - title: data.title.present ? data.title.value : this.title, - updatedUtc: data.updatedUtc.present - ? data.updatedUtc.value - : this.updatedUtc, - selfLink: data.selfLink.present ? data.selfLink.value : this.selfLink, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - providerListKind: data.providerListKind.present - ? data.providerListKind.value - : this.providerListKind, - isOwner: data.isOwner.present ? data.isOwner.value : this.isOwner, - isShared: data.isShared.present ? data.isShared.value : this.isShared, - deltaLink: data.deltaLink.present ? data.deltaLink.value : this.deltaLink, - providerMetadataJson: data.providerMetadataJson.present - ? data.providerMetadataJson.value - : this.providerMetadataJson, - serverMissing: data.serverMissing.present - ? data.serverMissing.value - : this.serverMissing, - localDirty: data.localDirty.present - ? data.localDirty.value - : this.localDirty, - pendingDelete: data.pendingDelete.present - ? data.pendingDelete.value - : this.pendingDelete, - lastSyncedAtUtc: data.lastSyncedAtUtc.present - ? data.lastSyncedAtUtc.value - : this.lastSyncedAtUtc, - createdLocalAtUtc: data.createdLocalAtUtc.present - ? data.createdLocalAtUtc.value - : this.createdLocalAtUtc, - updatedLocalAtUtc: data.updatedLocalAtUtc.present - ? data.updatedLocalAtUtc.value - : this.updatedLocalAtUtc, + canonicalServiceUri: data.canonicalServiceUri.present + ? data.canonicalServiceUri.value + : this.canonicalServiceUri, + canonicalOrigin: data.canonicalOrigin.present + ? data.canonicalOrigin.value + : this.canonicalOrigin, + principalHref: data.principalHref.present + ? data.principalHref.value + : this.principalHref, + calendarHomeHref: data.calendarHomeHref.present + ? data.calendarHomeHref.value + : this.calendarHomeHref, + calendarUserAddressesJson: data.calendarUserAddressesJson.present + ? data.calendarUserAddressesJson.value + : this.calendarUserAddressesJson, + scheduleInboxHref: data.scheduleInboxHref.present + ? data.scheduleInboxHref.value + : this.scheduleInboxHref, + scheduleOutboxHref: data.scheduleOutboxHref.present + ? data.scheduleOutboxHref.value + : this.scheduleOutboxHref, + capabilitiesJson: data.capabilitiesJson.present + ? data.capabilitiesJson.value + : this.capabilitiesJson, + capabilitiesSchemaVersion: data.capabilitiesSchemaVersion.present + ? data.capabilitiesSchemaVersion.value + : this.capabilitiesSchemaVersion, + providerProfileVersion: data.providerProfileVersion.present + ? data.providerProfileVersion.value + : this.providerProfileVersion, + discoveredAtUtc: data.discoveredAtUtc.present + ? data.discoveredAtUtc.value + : this.discoveredAtUtc, + lastValidatedAtUtc: data.lastValidatedAtUtc.present + ? data.lastValidatedAtUtc.value + : this.lastValidatedAtUtc, + lastDiscoveryErrorCode: data.lastDiscoveryErrorCode.present + ? data.lastDiscoveryErrorCode.value + : this.lastDiscoveryErrorCode, ); } @override String toString() { - return (StringBuffer('TaskList(') + return (StringBuffer('DavAccountService(') ..write('accountId: $accountId, ') - ..write('id: $id, ') - ..write('kind: $kind, ') - ..write('etag: $etag, ') - ..write('title: $title, ') - ..write('updatedUtc: $updatedUtc, ') - ..write('selfLink: $selfLink, ') - ..write('rawJson: $rawJson, ') - ..write('providerListKind: $providerListKind, ') - ..write('isOwner: $isOwner, ') - ..write('isShared: $isShared, ') - ..write('deltaLink: $deltaLink, ') - ..write('providerMetadataJson: $providerMetadataJson, ') - ..write('serverMissing: $serverMissing, ') - ..write('localDirty: $localDirty, ') - ..write('pendingDelete: $pendingDelete, ') - ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') - ..write('createdLocalAtUtc: $createdLocalAtUtc, ') - ..write('updatedLocalAtUtc: $updatedLocalAtUtc') + ..write('canonicalServiceUri: $canonicalServiceUri, ') + ..write('canonicalOrigin: $canonicalOrigin, ') + ..write('principalHref: $principalHref, ') + ..write('calendarHomeHref: $calendarHomeHref, ') + ..write('calendarUserAddressesJson: $calendarUserAddressesJson, ') + ..write('scheduleInboxHref: $scheduleInboxHref, ') + ..write('scheduleOutboxHref: $scheduleOutboxHref, ') + ..write('capabilitiesJson: $capabilitiesJson, ') + ..write('capabilitiesSchemaVersion: $capabilitiesSchemaVersion, ') + ..write('providerProfileVersion: $providerProfileVersion, ') + ..write('discoveredAtUtc: $discoveredAtUtc, ') + ..write('lastValidatedAtUtc: $lastValidatedAtUtc, ') + ..write('lastDiscoveryErrorCode: $lastDiscoveryErrorCode') ..write(')')) .toString(); } @@ -1787,209 +1823,172 @@ class TaskList extends DataClass implements Insertable { @override int get hashCode => Object.hash( accountId, - id, - kind, - etag, - title, - updatedUtc, - selfLink, - rawJson, - providerListKind, - isOwner, - isShared, - deltaLink, - providerMetadataJson, - serverMissing, - localDirty, - pendingDelete, - lastSyncedAtUtc, - createdLocalAtUtc, - updatedLocalAtUtc, + canonicalServiceUri, + canonicalOrigin, + principalHref, + calendarHomeHref, + calendarUserAddressesJson, + scheduleInboxHref, + scheduleOutboxHref, + capabilitiesJson, + capabilitiesSchemaVersion, + providerProfileVersion, + discoveredAtUtc, + lastValidatedAtUtc, + lastDiscoveryErrorCode, ); @override bool operator ==(Object other) => identical(this, other) || - (other is TaskList && + (other is DavAccountService && other.accountId == this.accountId && - other.id == this.id && - other.kind == this.kind && - other.etag == this.etag && - other.title == this.title && - other.updatedUtc == this.updatedUtc && - other.selfLink == this.selfLink && - other.rawJson == this.rawJson && - other.providerListKind == this.providerListKind && - other.isOwner == this.isOwner && - other.isShared == this.isShared && - other.deltaLink == this.deltaLink && - other.providerMetadataJson == this.providerMetadataJson && - other.serverMissing == this.serverMissing && - other.localDirty == this.localDirty && - other.pendingDelete == this.pendingDelete && - other.lastSyncedAtUtc == this.lastSyncedAtUtc && - other.createdLocalAtUtc == this.createdLocalAtUtc && - other.updatedLocalAtUtc == this.updatedLocalAtUtc); + other.canonicalServiceUri == this.canonicalServiceUri && + other.canonicalOrigin == this.canonicalOrigin && + other.principalHref == this.principalHref && + other.calendarHomeHref == this.calendarHomeHref && + other.calendarUserAddressesJson == this.calendarUserAddressesJson && + other.scheduleInboxHref == this.scheduleInboxHref && + other.scheduleOutboxHref == this.scheduleOutboxHref && + other.capabilitiesJson == this.capabilitiesJson && + other.capabilitiesSchemaVersion == this.capabilitiesSchemaVersion && + other.providerProfileVersion == this.providerProfileVersion && + other.discoveredAtUtc == this.discoveredAtUtc && + other.lastValidatedAtUtc == this.lastValidatedAtUtc && + other.lastDiscoveryErrorCode == this.lastDiscoveryErrorCode); } -class TaskListsCompanion extends UpdateCompanion { +class DavAccountServicesCompanion extends UpdateCompanion { final Value accountId; - final Value id; - final Value kind; - final Value etag; - final Value title; - final Value updatedUtc; - final Value selfLink; - final Value rawJson; - final Value providerListKind; - final Value isOwner; - final Value isShared; - final Value deltaLink; - final Value providerMetadataJson; - final Value serverMissing; - final Value localDirty; - final Value pendingDelete; - final Value lastSyncedAtUtc; - final Value createdLocalAtUtc; - final Value updatedLocalAtUtc; + final Value canonicalServiceUri; + final Value canonicalOrigin; + final Value principalHref; + final Value calendarHomeHref; + final Value calendarUserAddressesJson; + final Value scheduleInboxHref; + final Value scheduleOutboxHref; + final Value capabilitiesJson; + final Value capabilitiesSchemaVersion; + final Value providerProfileVersion; + final Value discoveredAtUtc; + final Value lastValidatedAtUtc; + final Value lastDiscoveryErrorCode; final Value rowid; - const TaskListsCompanion({ + const DavAccountServicesCompanion({ this.accountId = const Value.absent(), - this.id = const Value.absent(), - this.kind = const Value.absent(), - this.etag = const Value.absent(), - this.title = const Value.absent(), - this.updatedUtc = const Value.absent(), - this.selfLink = const Value.absent(), - this.rawJson = const Value.absent(), - this.providerListKind = const Value.absent(), - this.isOwner = const Value.absent(), - this.isShared = const Value.absent(), - this.deltaLink = const Value.absent(), - this.providerMetadataJson = const Value.absent(), - this.serverMissing = const Value.absent(), - this.localDirty = const Value.absent(), - this.pendingDelete = const Value.absent(), - this.lastSyncedAtUtc = const Value.absent(), - this.createdLocalAtUtc = const Value.absent(), - this.updatedLocalAtUtc = const Value.absent(), + this.canonicalServiceUri = const Value.absent(), + this.canonicalOrigin = const Value.absent(), + this.principalHref = const Value.absent(), + this.calendarHomeHref = const Value.absent(), + this.calendarUserAddressesJson = const Value.absent(), + this.scheduleInboxHref = const Value.absent(), + this.scheduleOutboxHref = const Value.absent(), + this.capabilitiesJson = const Value.absent(), + this.capabilitiesSchemaVersion = const Value.absent(), + this.providerProfileVersion = const Value.absent(), + this.discoveredAtUtc = const Value.absent(), + this.lastValidatedAtUtc = const Value.absent(), + this.lastDiscoveryErrorCode = const Value.absent(), this.rowid = const Value.absent(), }); - TaskListsCompanion.insert({ + DavAccountServicesCompanion.insert({ required String accountId, - required String id, - this.kind = const Value.absent(), - this.etag = const Value.absent(), - required String title, - this.updatedUtc = const Value.absent(), - this.selfLink = const Value.absent(), - required String rawJson, - this.providerListKind = const Value.absent(), - this.isOwner = const Value.absent(), - this.isShared = const Value.absent(), - this.deltaLink = const Value.absent(), - this.providerMetadataJson = const Value.absent(), - this.serverMissing = const Value.absent(), - this.localDirty = const Value.absent(), - this.pendingDelete = const Value.absent(), - this.lastSyncedAtUtc = const Value.absent(), - required String createdLocalAtUtc, - required String updatedLocalAtUtc, + required String canonicalServiceUri, + required String canonicalOrigin, + this.principalHref = const Value.absent(), + this.calendarHomeHref = const Value.absent(), + this.calendarUserAddressesJson = const Value.absent(), + this.scheduleInboxHref = const Value.absent(), + this.scheduleOutboxHref = const Value.absent(), + this.capabilitiesJson = const Value.absent(), + this.capabilitiesSchemaVersion = const Value.absent(), + this.providerProfileVersion = const Value.absent(), + required String discoveredAtUtc, + this.lastValidatedAtUtc = const Value.absent(), + this.lastDiscoveryErrorCode = const Value.absent(), this.rowid = const Value.absent(), }) : accountId = Value(accountId), - id = Value(id), - title = Value(title), - rawJson = Value(rawJson), - createdLocalAtUtc = Value(createdLocalAtUtc), - updatedLocalAtUtc = Value(updatedLocalAtUtc); - static Insertable custom({ + canonicalServiceUri = Value(canonicalServiceUri), + canonicalOrigin = Value(canonicalOrigin), + discoveredAtUtc = Value(discoveredAtUtc); + static Insertable custom({ Expression? accountId, - Expression? id, - Expression? kind, - Expression? etag, - Expression? title, - Expression? updatedUtc, - Expression? selfLink, - Expression? rawJson, - Expression? providerListKind, - Expression? isOwner, - Expression? isShared, - Expression? deltaLink, - Expression? providerMetadataJson, - Expression? serverMissing, - Expression? localDirty, - Expression? pendingDelete, - Expression? lastSyncedAtUtc, - Expression? createdLocalAtUtc, - Expression? updatedLocalAtUtc, + Expression? canonicalServiceUri, + Expression? canonicalOrigin, + Expression? principalHref, + Expression? calendarHomeHref, + Expression? calendarUserAddressesJson, + Expression? scheduleInboxHref, + Expression? scheduleOutboxHref, + Expression? capabilitiesJson, + Expression? capabilitiesSchemaVersion, + Expression? providerProfileVersion, + Expression? discoveredAtUtc, + Expression? lastValidatedAtUtc, + Expression? lastDiscoveryErrorCode, Expression? rowid, }) { return RawValuesInsertable({ if (accountId != null) 'account_id': accountId, - if (id != null) 'id': id, - if (kind != null) 'kind': kind, - if (etag != null) 'etag': etag, - if (title != null) 'title': title, - if (updatedUtc != null) 'updated_utc': updatedUtc, - if (selfLink != null) 'self_link': selfLink, - if (rawJson != null) 'raw_json': rawJson, - if (providerListKind != null) 'provider_list_kind': providerListKind, - if (isOwner != null) 'is_owner': isOwner, - if (isShared != null) 'is_shared': isShared, - if (deltaLink != null) 'delta_link': deltaLink, - if (providerMetadataJson != null) - 'provider_metadata_json': providerMetadataJson, - if (serverMissing != null) 'server_missing': serverMissing, - if (localDirty != null) 'local_dirty': localDirty, - if (pendingDelete != null) 'pending_delete': pendingDelete, - if (lastSyncedAtUtc != null) 'last_synced_at_utc': lastSyncedAtUtc, - if (createdLocalAtUtc != null) 'created_local_at_utc': createdLocalAtUtc, - if (updatedLocalAtUtc != null) 'updated_local_at_utc': updatedLocalAtUtc, + if (canonicalServiceUri != null) + 'canonical_service_uri': canonicalServiceUri, + if (canonicalOrigin != null) 'canonical_origin': canonicalOrigin, + if (principalHref != null) 'principal_href': principalHref, + if (calendarHomeHref != null) 'calendar_home_href': calendarHomeHref, + if (calendarUserAddressesJson != null) + 'calendar_user_addresses_json': calendarUserAddressesJson, + if (scheduleInboxHref != null) 'schedule_inbox_href': scheduleInboxHref, + if (scheduleOutboxHref != null) + 'schedule_outbox_href': scheduleOutboxHref, + if (capabilitiesJson != null) 'capabilities_json': capabilitiesJson, + if (capabilitiesSchemaVersion != null) + 'capabilities_schema_version': capabilitiesSchemaVersion, + if (providerProfileVersion != null) + 'provider_profile_version': providerProfileVersion, + if (discoveredAtUtc != null) 'discovered_at_utc': discoveredAtUtc, + if (lastValidatedAtUtc != null) + 'last_validated_at_utc': lastValidatedAtUtc, + if (lastDiscoveryErrorCode != null) + 'last_discovery_error_code': lastDiscoveryErrorCode, if (rowid != null) 'rowid': rowid, }); } - TaskListsCompanion copyWith({ + DavAccountServicesCompanion copyWith({ Value? accountId, - Value? id, - Value? kind, - Value? etag, - Value? title, - Value? updatedUtc, - Value? selfLink, - Value? rawJson, - Value? providerListKind, - Value? isOwner, - Value? isShared, - Value? deltaLink, - Value? providerMetadataJson, - Value? serverMissing, - Value? localDirty, - Value? pendingDelete, - Value? lastSyncedAtUtc, - Value? createdLocalAtUtc, - Value? updatedLocalAtUtc, + Value? canonicalServiceUri, + Value? canonicalOrigin, + Value? principalHref, + Value? calendarHomeHref, + Value? calendarUserAddressesJson, + Value? scheduleInboxHref, + Value? scheduleOutboxHref, + Value? capabilitiesJson, + Value? capabilitiesSchemaVersion, + Value? providerProfileVersion, + Value? discoveredAtUtc, + Value? lastValidatedAtUtc, + Value? lastDiscoveryErrorCode, Value? rowid, }) { - return TaskListsCompanion( + return DavAccountServicesCompanion( accountId: accountId ?? this.accountId, - id: id ?? this.id, - kind: kind ?? this.kind, - etag: etag ?? this.etag, - title: title ?? this.title, - updatedUtc: updatedUtc ?? this.updatedUtc, - selfLink: selfLink ?? this.selfLink, - rawJson: rawJson ?? this.rawJson, - providerListKind: providerListKind ?? this.providerListKind, - isOwner: isOwner ?? this.isOwner, - isShared: isShared ?? this.isShared, - deltaLink: deltaLink ?? this.deltaLink, - providerMetadataJson: providerMetadataJson ?? this.providerMetadataJson, - serverMissing: serverMissing ?? this.serverMissing, - localDirty: localDirty ?? this.localDirty, - pendingDelete: pendingDelete ?? this.pendingDelete, - lastSyncedAtUtc: lastSyncedAtUtc ?? this.lastSyncedAtUtc, - createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, - updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, + canonicalServiceUri: canonicalServiceUri ?? this.canonicalServiceUri, + canonicalOrigin: canonicalOrigin ?? this.canonicalOrigin, + principalHref: principalHref ?? this.principalHref, + calendarHomeHref: calendarHomeHref ?? this.calendarHomeHref, + calendarUserAddressesJson: + calendarUserAddressesJson ?? this.calendarUserAddressesJson, + scheduleInboxHref: scheduleInboxHref ?? this.scheduleInboxHref, + scheduleOutboxHref: scheduleOutboxHref ?? this.scheduleOutboxHref, + capabilitiesJson: capabilitiesJson ?? this.capabilitiesJson, + capabilitiesSchemaVersion: + capabilitiesSchemaVersion ?? this.capabilitiesSchemaVersion, + providerProfileVersion: + providerProfileVersion ?? this.providerProfileVersion, + discoveredAtUtc: discoveredAtUtc ?? this.discoveredAtUtc, + lastValidatedAtUtc: lastValidatedAtUtc ?? this.lastValidatedAtUtc, + lastDiscoveryErrorCode: + lastDiscoveryErrorCode ?? this.lastDiscoveryErrorCode, rowid: rowid ?? this.rowid, ); } @@ -2000,61 +1999,54 @@ class TaskListsCompanion extends UpdateCompanion { if (accountId.present) { map['account_id'] = Variable(accountId.value); } - if (id.present) { - map['id'] = Variable(id.value); - } - if (kind.present) { - map['kind'] = Variable(kind.value); - } - if (etag.present) { - map['etag'] = Variable(etag.value); - } - if (title.present) { - map['title'] = Variable(title.value); + if (canonicalServiceUri.present) { + map['canonical_service_uri'] = Variable( + canonicalServiceUri.value, + ); } - if (updatedUtc.present) { - map['updated_utc'] = Variable(updatedUtc.value); + if (canonicalOrigin.present) { + map['canonical_origin'] = Variable(canonicalOrigin.value); } - if (selfLink.present) { - map['self_link'] = Variable(selfLink.value); + if (principalHref.present) { + map['principal_href'] = Variable(principalHref.value); } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); + if (calendarHomeHref.present) { + map['calendar_home_href'] = Variable(calendarHomeHref.value); } - if (providerListKind.present) { - map['provider_list_kind'] = Variable(providerListKind.value); + if (calendarUserAddressesJson.present) { + map['calendar_user_addresses_json'] = Variable( + calendarUserAddressesJson.value, + ); } - if (isOwner.present) { - map['is_owner'] = Variable(isOwner.value); + if (scheduleInboxHref.present) { + map['schedule_inbox_href'] = Variable(scheduleInboxHref.value); } - if (isShared.present) { - map['is_shared'] = Variable(isShared.value); + if (scheduleOutboxHref.present) { + map['schedule_outbox_href'] = Variable(scheduleOutboxHref.value); } - if (deltaLink.present) { - map['delta_link'] = Variable(deltaLink.value); + if (capabilitiesJson.present) { + map['capabilities_json'] = Variable(capabilitiesJson.value); } - if (providerMetadataJson.present) { - map['provider_metadata_json'] = Variable( - providerMetadataJson.value, + if (capabilitiesSchemaVersion.present) { + map['capabilities_schema_version'] = Variable( + capabilitiesSchemaVersion.value, ); } - if (serverMissing.present) { - map['server_missing'] = Variable(serverMissing.value); - } - if (localDirty.present) { - map['local_dirty'] = Variable(localDirty.value); - } - if (pendingDelete.present) { - map['pending_delete'] = Variable(pendingDelete.value); + if (providerProfileVersion.present) { + map['provider_profile_version'] = Variable( + providerProfileVersion.value, + ); } - if (lastSyncedAtUtc.present) { - map['last_synced_at_utc'] = Variable(lastSyncedAtUtc.value); + if (discoveredAtUtc.present) { + map['discovered_at_utc'] = Variable(discoveredAtUtc.value); } - if (createdLocalAtUtc.present) { - map['created_local_at_utc'] = Variable(createdLocalAtUtc.value); + if (lastValidatedAtUtc.present) { + map['last_validated_at_utc'] = Variable(lastValidatedAtUtc.value); } - if (updatedLocalAtUtc.present) { - map['updated_local_at_utc'] = Variable(updatedLocalAtUtc.value); + if (lastDiscoveryErrorCode.present) { + map['last_discovery_error_code'] = Variable( + lastDiscoveryErrorCode.value, + ); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -2064,37 +2056,42 @@ class TaskListsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('TaskListsCompanion(') + return (StringBuffer('DavAccountServicesCompanion(') ..write('accountId: $accountId, ') - ..write('id: $id, ') - ..write('kind: $kind, ') - ..write('etag: $etag, ') - ..write('title: $title, ') - ..write('updatedUtc: $updatedUtc, ') - ..write('selfLink: $selfLink, ') - ..write('rawJson: $rawJson, ') - ..write('providerListKind: $providerListKind, ') - ..write('isOwner: $isOwner, ') - ..write('isShared: $isShared, ') - ..write('deltaLink: $deltaLink, ') - ..write('providerMetadataJson: $providerMetadataJson, ') - ..write('serverMissing: $serverMissing, ') - ..write('localDirty: $localDirty, ') - ..write('pendingDelete: $pendingDelete, ') - ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') - ..write('createdLocalAtUtc: $createdLocalAtUtc, ') - ..write('updatedLocalAtUtc: $updatedLocalAtUtc, ') + ..write('canonicalServiceUri: $canonicalServiceUri, ') + ..write('canonicalOrigin: $canonicalOrigin, ') + ..write('principalHref: $principalHref, ') + ..write('calendarHomeHref: $calendarHomeHref, ') + ..write('calendarUserAddressesJson: $calendarUserAddressesJson, ') + ..write('scheduleInboxHref: $scheduleInboxHref, ') + ..write('scheduleOutboxHref: $scheduleOutboxHref, ') + ..write('capabilitiesJson: $capabilitiesJson, ') + ..write('capabilitiesSchemaVersion: $capabilitiesSchemaVersion, ') + ..write('providerProfileVersion: $providerProfileVersion, ') + ..write('discoveredAtUtc: $discoveredAtUtc, ') + ..write('lastValidatedAtUtc: $lastValidatedAtUtc, ') + ..write('lastDiscoveryErrorCode: $lastDiscoveryErrorCode, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { +class $DavCollectionsTable extends DavCollections + with TableInfo<$DavCollectionsTable, DavCollection> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $TasksTable(this.attachedDatabase, [this._alias]); + $DavCollectionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _accountIdMeta = const VerificationMeta( 'accountId', ); @@ -2109,578 +2106,454 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { 'REFERENCES accounts (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _taskListIdMeta = const VerificationMeta( - 'taskListId', + static const VerificationMeta _hrefKeyMeta = const VerificationMeta( + 'hrefKey', ); @override - late final GeneratedColumn taskListId = GeneratedColumn( - 'task_list_id', + late final GeneratedColumn hrefKey = GeneratedColumn( + 'href_key', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _idMeta = const VerificationMeta('id'); + static const VerificationMeta _requestUriMeta = const VerificationMeta( + 'requestUri', + ); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', + late final GeneratedColumn requestUri = GeneratedColumn( + 'request_uri', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _kindMeta = const VerificationMeta('kind'); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); @override - late final GeneratedColumn kind = GeneratedColumn( - 'kind', + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, + ); + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', ); - static const VerificationMeta _etagMeta = const VerificationMeta('etag'); @override - late final GeneratedColumn etag = GeneratedColumn( - 'etag', + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _titleMeta = const VerificationMeta('title'); - @override - late final GeneratedColumn title = GeneratedColumn( - 'title', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedUtcMeta = const VerificationMeta( - 'updatedUtc', + static const VerificationMeta _resourceTypesJsonMeta = const VerificationMeta( + 'resourceTypesJson', ); @override - late final GeneratedColumn updatedUtc = GeneratedColumn( - 'updated_utc', + late final GeneratedColumn resourceTypesJson = + GeneratedColumn( + 'resource_types_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('[]'), + ); + static const VerificationMeta _supportedComponentMaskMeta = + const VerificationMeta('supportedComponentMask'); + @override + late final GeneratedColumn supportedComponentMask = GeneratedColumn( + 'supported_component_mask', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.int, requiredDuringInsert: false, + defaultValue: const Constant(0), ); - static const VerificationMeta _selfLinkMeta = const VerificationMeta( - 'selfLink', + static const VerificationMeta _supportedCalendarDataJsonMeta = + const VerificationMeta('supportedCalendarDataJson'); + @override + late final GeneratedColumn supportedCalendarDataJson = + GeneratedColumn( + 'supported_calendar_data_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('[]'), + ); + static const VerificationMeta _supportedReportsJsonMeta = + const VerificationMeta('supportedReportsJson'); + @override + late final GeneratedColumn supportedReportsJson = + GeneratedColumn( + 'supported_reports_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('[]'), + ); + static const VerificationMeta _currentUserPrivilegesJsonMeta = + const VerificationMeta('currentUserPrivilegesJson'); + @override + late final GeneratedColumn currentUserPrivilegesJson = + GeneratedColumn( + 'current_user_privileges_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('[]'), + ); + static const VerificationMeta _ownerHrefMeta = const VerificationMeta( + 'ownerHref', ); @override - late final GeneratedColumn selfLink = GeneratedColumn( - 'self_link', + late final GeneratedColumn ownerHref = GeneratedColumn( + 'owner_href', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _parentMeta = const VerificationMeta('parent'); + static const VerificationMeta _safeDisplayMetadataJsonMeta = + const VerificationMeta('safeDisplayMetadataJson'); @override - late final GeneratedColumn parent = GeneratedColumn( - 'parent', + late final GeneratedColumn safeDisplayMetadataJson = + GeneratedColumn( + 'safe_display_metadata_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _colorMeta = const VerificationMeta('color'); + @override + late final GeneratedColumn color = GeneratedColumn( + 'color', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _positionMeta = const VerificationMeta( - 'position', + static const VerificationMeta _sortOrderMeta = const VerificationMeta( + 'sortOrder', ); @override - late final GeneratedColumn position = GeneratedColumn( - 'position', + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + static const VerificationMeta _calendarTimeZoneMeta = const VerificationMeta( + 'calendarTimeZone', + ); @override - late final GeneratedColumn notes = GeneratedColumn( - 'notes', + late final GeneratedColumn calendarTimeZone = GeneratedColumn( + 'calendar_time_zone', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _statusMeta = const VerificationMeta('status'); + static const VerificationMeta _calendarTimeZoneIdMeta = + const VerificationMeta('calendarTimeZoneId'); @override - late final GeneratedColumn status = GeneratedColumn( - 'status', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _dueUtcMeta = const VerificationMeta('dueUtc'); + late final GeneratedColumn calendarTimeZoneId = + GeneratedColumn( + 'calendar_time_zone_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _scheduleTransparencyMeta = + const VerificationMeta('scheduleTransparency'); @override - late final GeneratedColumn dueUtc = GeneratedColumn( - 'due_utc', + late final GeneratedColumn scheduleTransparency = + GeneratedColumn( + 'schedule_transparency', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _maximumResourceSizeMeta = + const VerificationMeta('maximumResourceSize'); + @override + late final GeneratedColumn maximumResourceSize = GeneratedColumn( + 'maximum_resource_size', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _completedUtcMeta = const VerificationMeta( - 'completedUtc', + static const VerificationMeta _maximumInstancesMeta = const VerificationMeta( + 'maximumInstances', ); @override - late final GeneratedColumn completedUtc = GeneratedColumn( - 'completed_utc', + late final GeneratedColumn maximumInstances = GeneratedColumn( + 'maximum_instances', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.int, requiredDuringInsert: false, ); - static const VerificationMeta _providerStatusMeta = const VerificationMeta( - 'providerStatus', + static const VerificationMeta _syncTokenMeta = const VerificationMeta( + 'syncToken', ); @override - late final GeneratedColumn providerStatus = GeneratedColumn( - 'provider_status', + late final GeneratedColumn syncToken = GeneratedColumn( + 'sync_token', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _bodyContentMeta = const VerificationMeta( - 'bodyContent', - ); + static const VerificationMeta _ctagMeta = const VerificationMeta('ctag'); @override - late final GeneratedColumn bodyContent = GeneratedColumn( - 'body_content', + late final GeneratedColumn ctag = GeneratedColumn( + 'ctag', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _bodyContentTypeMeta = const VerificationMeta( - 'bodyContentType', + static const VerificationMeta _readOnlyMeta = const VerificationMeta( + 'readOnly', ); @override - late final GeneratedColumn bodyContentType = GeneratedColumn( - 'body_content_type', + late final GeneratedColumn readOnly = GeneratedColumn( + 'read_only', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("read_only" IN (0, 1))', + ), + defaultValue: const Constant(true), ); - static const VerificationMeta _microsoftDueDateTimeMeta = - const VerificationMeta('microsoftDueDateTime'); - @override - late final GeneratedColumn microsoftDueDateTime = - GeneratedColumn( - 'microsoft_due_date_time', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftDueTimeZoneMeta = - const VerificationMeta('microsoftDueTimeZone'); - @override - late final GeneratedColumn microsoftDueTimeZone = - GeneratedColumn( - 'microsoft_due_time_zone', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftStartDateTimeMeta = - const VerificationMeta('microsoftStartDateTime'); - @override - late final GeneratedColumn microsoftStartDateTime = - GeneratedColumn( - 'microsoft_start_date_time', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftStartTimeZoneMeta = - const VerificationMeta('microsoftStartTimeZone'); - @override - late final GeneratedColumn microsoftStartTimeZone = - GeneratedColumn( - 'microsoft_start_time_zone', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftReminderDateTimeMeta = - const VerificationMeta('microsoftReminderDateTime'); - @override - late final GeneratedColumn microsoftReminderDateTime = - GeneratedColumn( - 'microsoft_reminder_date_time', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftReminderTimeZoneMeta = - const VerificationMeta('microsoftReminderTimeZone'); - @override - late final GeneratedColumn microsoftReminderTimeZone = - GeneratedColumn( - 'microsoft_reminder_time_zone', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftIsReminderOnMeta = - const VerificationMeta('microsoftIsReminderOn'); + static const VerificationMeta _eventProjectionEnabledMeta = + const VerificationMeta('eventProjectionEnabled'); @override - late final GeneratedColumn microsoftIsReminderOn = + late final GeneratedColumn eventProjectionEnabled = GeneratedColumn( - 'microsoft_is_reminder_on', + 'event_projection_enabled', aliasedName, - true, + false, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("microsoft_is_reminder_on" IN (0, 1))', + 'CHECK ("event_projection_enabled" IN (0, 1))', ), + defaultValue: const Constant(false), ); - static const VerificationMeta _microsoftCompletedDateTimeMeta = - const VerificationMeta('microsoftCompletedDateTime'); - @override - late final GeneratedColumn microsoftCompletedDateTime = - GeneratedColumn( - 'microsoft_completed_date_time', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _microsoftCompletedTimeZoneMeta = - const VerificationMeta('microsoftCompletedTimeZone'); + static const VerificationMeta _taskProjectionEnabledMeta = + const VerificationMeta('taskProjectionEnabled'); @override - late final GeneratedColumn microsoftCompletedTimeZone = - GeneratedColumn( - 'microsoft_completed_time_zone', + late final GeneratedColumn taskProjectionEnabled = + GeneratedColumn( + 'task_projection_enabled', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("task_projection_enabled" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _recurrenceJsonMeta = const VerificationMeta( - 'recurrenceJson', + static const VerificationMeta _eventsSelectedMeta = const VerificationMeta( + 'eventsSelected', ); @override - late final GeneratedColumn recurrenceJson = GeneratedColumn( - 'recurrence_json', + late final GeneratedColumn eventsSelected = GeneratedColumn( + 'events_selected', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("events_selected" IN (0, 1))', + ), + defaultValue: const Constant(true), ); - static const VerificationMeta _importanceMeta = const VerificationMeta( - 'importance', + static const VerificationMeta _tasksSelectedMeta = const VerificationMeta( + 'tasksSelected', ); @override - late final GeneratedColumn importance = GeneratedColumn( - 'importance', + late final GeneratedColumn tasksSelected = GeneratedColumn( + 'tasks_selected', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("tasks_selected" IN (0, 1))', + ), + defaultValue: const Constant(true), ); - static const VerificationMeta _categoriesJsonMeta = const VerificationMeta( - 'categoriesJson', + static const VerificationMeta _serverMissingMeta = const VerificationMeta( + 'serverMissing', ); @override - late final GeneratedColumn categoriesJson = GeneratedColumn( - 'categories_json', + late final GeneratedColumn serverMissing = GeneratedColumn( + 'server_missing', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("server_missing" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _hasAttachmentsMeta = const VerificationMeta( - 'hasAttachments', + static const VerificationMeta _deletedMeta = const VerificationMeta( + 'deleted', ); @override - late final GeneratedColumn hasAttachments = GeneratedColumn( - 'has_attachments', + late final GeneratedColumn deleted = GeneratedColumn( + 'deleted', aliasedName, - true, + false, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("has_attachments" IN (0, 1))', + 'CHECK ("deleted" IN (0, 1))', ), + defaultValue: const Constant(false), ); - static const VerificationMeta _providerMetadataJsonMeta = - const VerificationMeta('providerMetadataJson'); + static const VerificationMeta _lastInventoryAtUtcMeta = + const VerificationMeta('lastInventoryAtUtc'); @override - late final GeneratedColumn providerMetadataJson = + late final GeneratedColumn lastInventoryAtUtc = GeneratedColumn( - 'provider_metadata_json', + 'last_inventory_at_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _deletedMeta = const VerificationMeta( - 'deleted', + static const VerificationMeta _lastSyncAtUtcMeta = const VerificationMeta( + 'lastSyncAtUtc', ); @override - late final GeneratedColumn deleted = GeneratedColumn( - 'deleted', + late final GeneratedColumn lastSyncAtUtc = GeneratedColumn( + 'last_sync_at_utc', aliasedName, true, - type: DriftSqlType.bool, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("deleted" IN (0, 1))', - ), ); - static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); + static const VerificationMeta _parserVersionMeta = const VerificationMeta( + 'parserVersion', + ); @override - late final GeneratedColumn hidden = GeneratedColumn( - 'hidden', + late final GeneratedColumn parserVersion = GeneratedColumn( + 'parser_version', aliasedName, - true, - type: DriftSqlType.bool, + false, + type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("hidden" IN (0, 1))', - ), + defaultValue: const Constant(1), ); - static const VerificationMeta _linksJsonMeta = const VerificationMeta( - 'linksJson', + static const VerificationMeta _projectionVersionMeta = const VerificationMeta( + 'projectionVersion', ); @override - late final GeneratedColumn linksJson = GeneratedColumn( - 'links_json', + late final GeneratedColumn projectionVersion = GeneratedColumn( + 'projection_version', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.int, requiredDuringInsert: false, + defaultValue: const Constant(1), ); - static const VerificationMeta _webViewLinkMeta = const VerificationMeta( - 'webViewLink', + static const VerificationMeta _createdAtUtcMeta = const VerificationMeta( + 'createdAtUtc', ); @override - late final GeneratedColumn webViewLink = GeneratedColumn( - 'web_view_link', + late final GeneratedColumn createdAtUtc = GeneratedColumn( + 'created_at_utc', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtUtcMeta = const VerificationMeta( + 'updatedAtUtc', ); - static const VerificationMeta _assignmentInfoJsonMeta = - const VerificationMeta('assignmentInfoJson'); @override - late final GeneratedColumn assignmentInfoJson = - GeneratedColumn( - 'assignment_info_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', - ); - @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', + late final GeneratedColumn updatedAtUtc = GeneratedColumn( + 'updated_at_utc', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _serverMissingMeta = const VerificationMeta( - 'serverMissing', - ); - @override - late final GeneratedColumn serverMissing = GeneratedColumn( - 'server_missing', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("server_missing" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _localDirtyMeta = const VerificationMeta( - 'localDirty', - ); - @override - late final GeneratedColumn localDirty = GeneratedColumn( - 'local_dirty', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("local_dirty" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _pendingDeleteMeta = const VerificationMeta( - 'pendingDelete', - ); - @override - late final GeneratedColumn pendingDelete = GeneratedColumn( - 'pending_delete', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("pending_delete" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _pendingMoveMeta = const VerificationMeta( - 'pendingMove', - ); - @override - late final GeneratedColumn pendingMove = GeneratedColumn( - 'pending_move', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("pending_move" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _localCreatedMeta = const VerificationMeta( - 'localCreated', - ); - @override - late final GeneratedColumn localCreated = GeneratedColumn( - 'local_created', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("local_created" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _syncBaseUpdatedUtcMeta = - const VerificationMeta('syncBaseUpdatedUtc'); - @override - late final GeneratedColumn syncBaseUpdatedUtc = - GeneratedColumn( - 'sync_base_updated_utc', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _lastSyncedAtUtcMeta = const VerificationMeta( - 'lastSyncedAtUtc', - ); - @override - late final GeneratedColumn lastSyncedAtUtc = GeneratedColumn( - 'last_synced_at_utc', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _createdLocalAtUtcMeta = const VerificationMeta( - 'createdLocalAtUtc', - ); - @override - late final GeneratedColumn createdLocalAtUtc = - GeneratedColumn( - 'created_local_at_utc', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedLocalAtUtcMeta = const VerificationMeta( - 'updatedLocalAtUtc', - ); - @override - late final GeneratedColumn updatedLocalAtUtc = - GeneratedColumn( - 'updated_local_at_utc', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); @override List get $columns => [ - accountId, - taskListId, id, - kind, - etag, - title, - updatedUtc, - selfLink, - parent, - position, - notes, - status, - dueUtc, - completedUtc, - providerStatus, - bodyContent, - bodyContentType, - microsoftDueDateTime, - microsoftDueTimeZone, - microsoftStartDateTime, - microsoftStartTimeZone, - microsoftReminderDateTime, - microsoftReminderTimeZone, - microsoftIsReminderOn, - microsoftCompletedDateTime, - microsoftCompletedTimeZone, - recurrenceJson, - importance, - categoriesJson, - hasAttachments, - providerMetadataJson, - deleted, - hidden, - linksJson, - webViewLink, - assignmentInfoJson, - rawJson, + accountId, + hrefKey, + requestUri, + displayName, + description, + resourceTypesJson, + supportedComponentMask, + supportedCalendarDataJson, + supportedReportsJson, + currentUserPrivilegesJson, + ownerHref, + safeDisplayMetadataJson, + color, + sortOrder, + calendarTimeZone, + calendarTimeZoneId, + scheduleTransparency, + maximumResourceSize, + maximumInstances, + syncToken, + ctag, + readOnly, + eventProjectionEnabled, + taskProjectionEnabled, + eventsSelected, + tasksSelected, serverMissing, - localDirty, - pendingDelete, - pendingMove, - localCreated, - syncBaseUpdatedUtc, - lastSyncedAtUtc, - createdLocalAtUtc, - updatedLocalAtUtc, + deleted, + lastInventoryAtUtc, + lastSyncAtUtc, + parserVersion, + projectionVersion, + createdAtUtc, + updatedAtUtc, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'tasks'; + static const String $name = 'dav_collections'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } if (data.containsKey('account_id')) { context.handle( _accountIdMeta, @@ -2689,287 +2562,213 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('task_list_id')) { + if (data.containsKey('href_key')) { context.handle( - _taskListIdMeta, - taskListId.isAcceptableOrUnknown( - data['task_list_id']!, - _taskListIdMeta, - ), + _hrefKeyMeta, + hrefKey.isAcceptableOrUnknown(data['href_key']!, _hrefKeyMeta), ); } else if (isInserting) { - context.missing(_taskListIdMeta); + context.missing(_hrefKeyMeta); } - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + if (data.containsKey('request_uri')) { + context.handle( + _requestUriMeta, + requestUri.isAcceptableOrUnknown(data['request_uri']!, _requestUriMeta), + ); } else if (isInserting) { - context.missing(_idMeta); + context.missing(_requestUriMeta); } - if (data.containsKey('kind')) { + if (data.containsKey('display_name')) { context.handle( - _kindMeta, - kind.isAcceptableOrUnknown(data['kind']!, _kindMeta), + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), ); + } else if (isInserting) { + context.missing(_displayNameMeta); } - if (data.containsKey('etag')) { + if (data.containsKey('description')) { context.handle( - _etagMeta, - etag.isAcceptableOrUnknown(data['etag']!, _etagMeta), + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), ); } - if (data.containsKey('title')) { + if (data.containsKey('resource_types_json')) { context.handle( - _titleMeta, - title.isAcceptableOrUnknown(data['title']!, _titleMeta), + _resourceTypesJsonMeta, + resourceTypesJson.isAcceptableOrUnknown( + data['resource_types_json']!, + _resourceTypesJsonMeta, + ), ); - } else if (isInserting) { - context.missing(_titleMeta); } - if (data.containsKey('updated_utc')) { + if (data.containsKey('supported_component_mask')) { context.handle( - _updatedUtcMeta, - updatedUtc.isAcceptableOrUnknown(data['updated_utc']!, _updatedUtcMeta), + _supportedComponentMaskMeta, + supportedComponentMask.isAcceptableOrUnknown( + data['supported_component_mask']!, + _supportedComponentMaskMeta, + ), ); } - if (data.containsKey('self_link')) { + if (data.containsKey('supported_calendar_data_json')) { context.handle( - _selfLinkMeta, - selfLink.isAcceptableOrUnknown(data['self_link']!, _selfLinkMeta), + _supportedCalendarDataJsonMeta, + supportedCalendarDataJson.isAcceptableOrUnknown( + data['supported_calendar_data_json']!, + _supportedCalendarDataJsonMeta, + ), ); } - if (data.containsKey('parent')) { + if (data.containsKey('supported_reports_json')) { context.handle( - _parentMeta, - parent.isAcceptableOrUnknown(data['parent']!, _parentMeta), + _supportedReportsJsonMeta, + supportedReportsJson.isAcceptableOrUnknown( + data['supported_reports_json']!, + _supportedReportsJsonMeta, + ), ); } - if (data.containsKey('position')) { + if (data.containsKey('current_user_privileges_json')) { context.handle( - _positionMeta, - position.isAcceptableOrUnknown(data['position']!, _positionMeta), + _currentUserPrivilegesJsonMeta, + currentUserPrivilegesJson.isAcceptableOrUnknown( + data['current_user_privileges_json']!, + _currentUserPrivilegesJsonMeta, + ), ); } - if (data.containsKey('notes')) { + if (data.containsKey('owner_href')) { context.handle( - _notesMeta, - notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + _ownerHrefMeta, + ownerHref.isAcceptableOrUnknown(data['owner_href']!, _ownerHrefMeta), ); } - if (data.containsKey('status')) { + if (data.containsKey('safe_display_metadata_json')) { context.handle( - _statusMeta, - status.isAcceptableOrUnknown(data['status']!, _statusMeta), + _safeDisplayMetadataJsonMeta, + safeDisplayMetadataJson.isAcceptableOrUnknown( + data['safe_display_metadata_json']!, + _safeDisplayMetadataJsonMeta, + ), ); } - if (data.containsKey('due_utc')) { + if (data.containsKey('color')) { context.handle( - _dueUtcMeta, - dueUtc.isAcceptableOrUnknown(data['due_utc']!, _dueUtcMeta), + _colorMeta, + color.isAcceptableOrUnknown(data['color']!, _colorMeta), ); } - if (data.containsKey('completed_utc')) { + if (data.containsKey('sort_order')) { context.handle( - _completedUtcMeta, - completedUtc.isAcceptableOrUnknown( - data['completed_utc']!, - _completedUtcMeta, - ), - ); - } - if (data.containsKey('provider_status')) { - context.handle( - _providerStatusMeta, - providerStatus.isAcceptableOrUnknown( - data['provider_status']!, - _providerStatusMeta, - ), - ); - } - if (data.containsKey('body_content')) { - context.handle( - _bodyContentMeta, - bodyContent.isAcceptableOrUnknown( - data['body_content']!, - _bodyContentMeta, - ), - ); - } - if (data.containsKey('body_content_type')) { - context.handle( - _bodyContentTypeMeta, - bodyContentType.isAcceptableOrUnknown( - data['body_content_type']!, - _bodyContentTypeMeta, - ), - ); - } - if (data.containsKey('microsoft_due_date_time')) { - context.handle( - _microsoftDueDateTimeMeta, - microsoftDueDateTime.isAcceptableOrUnknown( - data['microsoft_due_date_time']!, - _microsoftDueDateTimeMeta, - ), - ); - } - if (data.containsKey('microsoft_due_time_zone')) { - context.handle( - _microsoftDueTimeZoneMeta, - microsoftDueTimeZone.isAcceptableOrUnknown( - data['microsoft_due_time_zone']!, - _microsoftDueTimeZoneMeta, - ), - ); - } - if (data.containsKey('microsoft_start_date_time')) { - context.handle( - _microsoftStartDateTimeMeta, - microsoftStartDateTime.isAcceptableOrUnknown( - data['microsoft_start_date_time']!, - _microsoftStartDateTimeMeta, - ), - ); - } - if (data.containsKey('microsoft_start_time_zone')) { - context.handle( - _microsoftStartTimeZoneMeta, - microsoftStartTimeZone.isAcceptableOrUnknown( - data['microsoft_start_time_zone']!, - _microsoftStartTimeZoneMeta, - ), + _sortOrderMeta, + sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta), ); } - if (data.containsKey('microsoft_reminder_date_time')) { + if (data.containsKey('calendar_time_zone')) { context.handle( - _microsoftReminderDateTimeMeta, - microsoftReminderDateTime.isAcceptableOrUnknown( - data['microsoft_reminder_date_time']!, - _microsoftReminderDateTimeMeta, + _calendarTimeZoneMeta, + calendarTimeZone.isAcceptableOrUnknown( + data['calendar_time_zone']!, + _calendarTimeZoneMeta, ), ); } - if (data.containsKey('microsoft_reminder_time_zone')) { + if (data.containsKey('calendar_time_zone_id')) { context.handle( - _microsoftReminderTimeZoneMeta, - microsoftReminderTimeZone.isAcceptableOrUnknown( - data['microsoft_reminder_time_zone']!, - _microsoftReminderTimeZoneMeta, + _calendarTimeZoneIdMeta, + calendarTimeZoneId.isAcceptableOrUnknown( + data['calendar_time_zone_id']!, + _calendarTimeZoneIdMeta, ), ); } - if (data.containsKey('microsoft_is_reminder_on')) { + if (data.containsKey('schedule_transparency')) { context.handle( - _microsoftIsReminderOnMeta, - microsoftIsReminderOn.isAcceptableOrUnknown( - data['microsoft_is_reminder_on']!, - _microsoftIsReminderOnMeta, + _scheduleTransparencyMeta, + scheduleTransparency.isAcceptableOrUnknown( + data['schedule_transparency']!, + _scheduleTransparencyMeta, ), ); } - if (data.containsKey('microsoft_completed_date_time')) { + if (data.containsKey('maximum_resource_size')) { context.handle( - _microsoftCompletedDateTimeMeta, - microsoftCompletedDateTime.isAcceptableOrUnknown( - data['microsoft_completed_date_time']!, - _microsoftCompletedDateTimeMeta, + _maximumResourceSizeMeta, + maximumResourceSize.isAcceptableOrUnknown( + data['maximum_resource_size']!, + _maximumResourceSizeMeta, ), ); } - if (data.containsKey('microsoft_completed_time_zone')) { + if (data.containsKey('maximum_instances')) { context.handle( - _microsoftCompletedTimeZoneMeta, - microsoftCompletedTimeZone.isAcceptableOrUnknown( - data['microsoft_completed_time_zone']!, - _microsoftCompletedTimeZoneMeta, + _maximumInstancesMeta, + maximumInstances.isAcceptableOrUnknown( + data['maximum_instances']!, + _maximumInstancesMeta, ), ); } - if (data.containsKey('recurrence_json')) { + if (data.containsKey('sync_token')) { context.handle( - _recurrenceJsonMeta, - recurrenceJson.isAcceptableOrUnknown( - data['recurrence_json']!, - _recurrenceJsonMeta, - ), + _syncTokenMeta, + syncToken.isAcceptableOrUnknown(data['sync_token']!, _syncTokenMeta), ); } - if (data.containsKey('importance')) { + if (data.containsKey('ctag')) { context.handle( - _importanceMeta, - importance.isAcceptableOrUnknown(data['importance']!, _importanceMeta), + _ctagMeta, + ctag.isAcceptableOrUnknown(data['ctag']!, _ctagMeta), ); } - if (data.containsKey('categories_json')) { + if (data.containsKey('read_only')) { context.handle( - _categoriesJsonMeta, - categoriesJson.isAcceptableOrUnknown( - data['categories_json']!, - _categoriesJsonMeta, - ), + _readOnlyMeta, + readOnly.isAcceptableOrUnknown(data['read_only']!, _readOnlyMeta), ); } - if (data.containsKey('has_attachments')) { + if (data.containsKey('event_projection_enabled')) { context.handle( - _hasAttachmentsMeta, - hasAttachments.isAcceptableOrUnknown( - data['has_attachments']!, - _hasAttachmentsMeta, + _eventProjectionEnabledMeta, + eventProjectionEnabled.isAcceptableOrUnknown( + data['event_projection_enabled']!, + _eventProjectionEnabledMeta, ), ); } - if (data.containsKey('provider_metadata_json')) { + if (data.containsKey('task_projection_enabled')) { context.handle( - _providerMetadataJsonMeta, - providerMetadataJson.isAcceptableOrUnknown( - data['provider_metadata_json']!, - _providerMetadataJsonMeta, + _taskProjectionEnabledMeta, + taskProjectionEnabled.isAcceptableOrUnknown( + data['task_projection_enabled']!, + _taskProjectionEnabledMeta, ), ); } - if (data.containsKey('deleted')) { - context.handle( - _deletedMeta, - deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta), - ); - } - if (data.containsKey('hidden')) { - context.handle( - _hiddenMeta, - hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta), - ); - } - if (data.containsKey('links_json')) { - context.handle( - _linksJsonMeta, - linksJson.isAcceptableOrUnknown(data['links_json']!, _linksJsonMeta), - ); - } - if (data.containsKey('web_view_link')) { + if (data.containsKey('events_selected')) { context.handle( - _webViewLinkMeta, - webViewLink.isAcceptableOrUnknown( - data['web_view_link']!, - _webViewLinkMeta, + _eventsSelectedMeta, + eventsSelected.isAcceptableOrUnknown( + data['events_selected']!, + _eventsSelectedMeta, ), ); } - if (data.containsKey('assignment_info_json')) { + if (data.containsKey('tasks_selected')) { context.handle( - _assignmentInfoJsonMeta, - assignmentInfoJson.isAcceptableOrUnknown( - data['assignment_info_json']!, - _assignmentInfoJsonMeta, + _tasksSelectedMeta, + tasksSelected.isAcceptableOrUnknown( + data['tasks_selected']!, + _tasksSelectedMeta, ), ); } - if (data.containsKey('raw_json')) { - context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), - ); - } else if (isInserting) { - context.missing(_rawJsonMeta); - } if (data.containsKey('server_missing')) { context.handle( _serverMissingMeta, @@ -2979,1535 +2778,1147 @@ class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { ), ); } - if (data.containsKey('local_dirty')) { - context.handle( - _localDirtyMeta, - localDirty.isAcceptableOrUnknown(data['local_dirty']!, _localDirtyMeta), - ); - } - if (data.containsKey('pending_delete')) { + if (data.containsKey('deleted')) { context.handle( - _pendingDeleteMeta, - pendingDelete.isAcceptableOrUnknown( - data['pending_delete']!, - _pendingDeleteMeta, - ), + _deletedMeta, + deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta), ); } - if (data.containsKey('pending_move')) { + if (data.containsKey('last_inventory_at_utc')) { context.handle( - _pendingMoveMeta, - pendingMove.isAcceptableOrUnknown( - data['pending_move']!, - _pendingMoveMeta, + _lastInventoryAtUtcMeta, + lastInventoryAtUtc.isAcceptableOrUnknown( + data['last_inventory_at_utc']!, + _lastInventoryAtUtcMeta, ), ); } - if (data.containsKey('local_created')) { + if (data.containsKey('last_sync_at_utc')) { context.handle( - _localCreatedMeta, - localCreated.isAcceptableOrUnknown( - data['local_created']!, - _localCreatedMeta, + _lastSyncAtUtcMeta, + lastSyncAtUtc.isAcceptableOrUnknown( + data['last_sync_at_utc']!, + _lastSyncAtUtcMeta, ), ); } - if (data.containsKey('sync_base_updated_utc')) { + if (data.containsKey('parser_version')) { context.handle( - _syncBaseUpdatedUtcMeta, - syncBaseUpdatedUtc.isAcceptableOrUnknown( - data['sync_base_updated_utc']!, - _syncBaseUpdatedUtcMeta, + _parserVersionMeta, + parserVersion.isAcceptableOrUnknown( + data['parser_version']!, + _parserVersionMeta, ), ); } - if (data.containsKey('last_synced_at_utc')) { + if (data.containsKey('projection_version')) { context.handle( - _lastSyncedAtUtcMeta, - lastSyncedAtUtc.isAcceptableOrUnknown( - data['last_synced_at_utc']!, - _lastSyncedAtUtcMeta, + _projectionVersionMeta, + projectionVersion.isAcceptableOrUnknown( + data['projection_version']!, + _projectionVersionMeta, ), ); } - if (data.containsKey('created_local_at_utc')) { + if (data.containsKey('created_at_utc')) { context.handle( - _createdLocalAtUtcMeta, - createdLocalAtUtc.isAcceptableOrUnknown( - data['created_local_at_utc']!, - _createdLocalAtUtcMeta, + _createdAtUtcMeta, + createdAtUtc.isAcceptableOrUnknown( + data['created_at_utc']!, + _createdAtUtcMeta, ), ); } else if (isInserting) { - context.missing(_createdLocalAtUtcMeta); + context.missing(_createdAtUtcMeta); } - if (data.containsKey('updated_local_at_utc')) { + if (data.containsKey('updated_at_utc')) { context.handle( - _updatedLocalAtUtcMeta, - updatedLocalAtUtc.isAcceptableOrUnknown( - data['updated_local_at_utc']!, - _updatedLocalAtUtcMeta, + _updatedAtUtcMeta, + updatedAtUtc.isAcceptableOrUnknown( + data['updated_at_utc']!, + _updatedAtUtcMeta, ), ); } else if (isInserting) { - context.missing(_updatedLocalAtUtcMeta); + context.missing(_updatedAtUtcMeta); } return context; } @override - Set get $primaryKey => {accountId, taskListId, id}; + Set get $primaryKey => {id}; @override - Task map(Map data, {String? tablePrefix}) { + DavCollection map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return Task( + return DavCollection( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, accountId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - taskListId: attachedDatabase.typeMapping.read( + hrefKey: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}task_list_id'], + data['${effectivePrefix}href_key'], )!, - id: attachedDatabase.typeMapping.read( + requestUri: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}id'], + data['${effectivePrefix}request_uri'], )!, - kind: attachedDatabase.typeMapping.read( + displayName: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}kind'], - ), - etag: attachedDatabase.typeMapping.read( + data['${effectivePrefix}display_name'], + )!, + description: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}etag'], + data['${effectivePrefix}description'], ), - title: attachedDatabase.typeMapping.read( + resourceTypesJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}title'], + data['${effectivePrefix}resource_types_json'], )!, - updatedUtc: attachedDatabase.typeMapping.read( + supportedComponentMask: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}supported_component_mask'], + )!, + supportedCalendarDataJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_utc'], - ), - selfLink: attachedDatabase.typeMapping.read( + data['${effectivePrefix}supported_calendar_data_json'], + )!, + supportedReportsJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}self_link'], - ), - parent: attachedDatabase.typeMapping.read( + data['${effectivePrefix}supported_reports_json'], + )!, + currentUserPrivilegesJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}parent'], + data['${effectivePrefix}current_user_privileges_json'], + )!, + ownerHref: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_href'], ), - position: attachedDatabase.typeMapping.read( + safeDisplayMetadataJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}position'], + data['${effectivePrefix}safe_display_metadata_json'], ), - notes: attachedDatabase.typeMapping.read( + color: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}notes'], - ), - status: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}status'], - ), - dueUtc: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}due_utc'], - ), - completedUtc: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}completed_utc'], - ), - providerStatus: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider_status'], - ), - bodyContent: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}body_content'], - ), - bodyContentType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}body_content_type'], - ), - microsoftDueDateTime: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_due_date_time'], - ), - microsoftDueTimeZone: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_due_time_zone'], - ), - microsoftStartDateTime: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_start_date_time'], - ), - microsoftStartTimeZone: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_start_time_zone'], + data['${effectivePrefix}color'], ), - microsoftReminderDateTime: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_reminder_date_time'], + sortOrder: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sort_order'], ), - microsoftReminderTimeZone: attachedDatabase.typeMapping.read( + calendarTimeZone: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}microsoft_reminder_time_zone'], - ), - microsoftIsReminderOn: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}microsoft_is_reminder_on'], + data['${effectivePrefix}calendar_time_zone'], ), - microsoftCompletedDateTime: attachedDatabase.typeMapping.read( + calendarTimeZoneId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}microsoft_completed_date_time'], + data['${effectivePrefix}calendar_time_zone_id'], ), - microsoftCompletedTimeZone: attachedDatabase.typeMapping.read( + scheduleTransparency: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}microsoft_completed_time_zone'], + data['${effectivePrefix}schedule_transparency'], ), - recurrenceJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}recurrence_json'], + maximumResourceSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}maximum_resource_size'], ), - importance: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}importance'], + maximumInstances: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}maximum_instances'], ), - categoriesJson: attachedDatabase.typeMapping.read( + syncToken: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}categories_json'], - ), - hasAttachments: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}has_attachments'], + data['${effectivePrefix}sync_token'], ), - providerMetadataJson: attachedDatabase.typeMapping.read( + ctag: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_metadata_json'], + data['${effectivePrefix}ctag'], ), - deleted: attachedDatabase.typeMapping.read( + readOnly: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}deleted'], - ), - hidden: attachedDatabase.typeMapping.read( + data['${effectivePrefix}read_only'], + )!, + eventProjectionEnabled: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}hidden'], - ), - linksJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}links_json'], - ), - webViewLink: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}web_view_link'], - ), - assignmentInfoJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}assignment_info_json'], - ), - rawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_json'], + data['${effectivePrefix}event_projection_enabled'], )!, - serverMissing: attachedDatabase.typeMapping.read( + taskProjectionEnabled: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}server_missing'], + data['${effectivePrefix}task_projection_enabled'], )!, - localDirty: attachedDatabase.typeMapping.read( + eventsSelected: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}local_dirty'], + data['${effectivePrefix}events_selected'], )!, - pendingDelete: attachedDatabase.typeMapping.read( + tasksSelected: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}pending_delete'], + data['${effectivePrefix}tasks_selected'], )!, - pendingMove: attachedDatabase.typeMapping.read( + serverMissing: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}pending_move'], + data['${effectivePrefix}server_missing'], )!, - localCreated: attachedDatabase.typeMapping.read( + deleted: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}local_created'], + data['${effectivePrefix}deleted'], )!, - syncBaseUpdatedUtc: attachedDatabase.typeMapping.read( + lastInventoryAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}sync_base_updated_utc'], + data['${effectivePrefix}last_inventory_at_utc'], ), - lastSyncedAtUtc: attachedDatabase.typeMapping.read( + lastSyncAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}last_synced_at_utc'], + data['${effectivePrefix}last_sync_at_utc'], ), - createdLocalAtUtc: attachedDatabase.typeMapping.read( + parserVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}parser_version'], + )!, + projectionVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}projection_version'], + )!, + createdAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}created_local_at_utc'], + data['${effectivePrefix}created_at_utc'], )!, - updatedLocalAtUtc: attachedDatabase.typeMapping.read( + updatedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_local_at_utc'], + data['${effectivePrefix}updated_at_utc'], )!, ); } @override - $TasksTable createAlias(String alias) { - return $TasksTable(attachedDatabase, alias); + $DavCollectionsTable createAlias(String alias) { + return $DavCollectionsTable(attachedDatabase, alias); } } -class Task extends DataClass implements Insertable { - final String accountId; - final String taskListId; +class DavCollection extends DataClass implements Insertable { final String id; - final String? kind; - final String? etag; - final String title; - final String? updatedUtc; - final String? selfLink; - final String? parent; - final String? position; - final String? notes; - final String? status; - final String? dueUtc; - final String? completedUtc; - final String? providerStatus; - final String? bodyContent; - final String? bodyContentType; - final String? microsoftDueDateTime; - final String? microsoftDueTimeZone; - final String? microsoftStartDateTime; - final String? microsoftStartTimeZone; - final String? microsoftReminderDateTime; - final String? microsoftReminderTimeZone; - final bool? microsoftIsReminderOn; - final String? microsoftCompletedDateTime; - final String? microsoftCompletedTimeZone; - final String? recurrenceJson; - final String? importance; - final String? categoriesJson; - final bool? hasAttachments; - final String? providerMetadataJson; - final bool? deleted; - final bool? hidden; - final String? linksJson; - final String? webViewLink; - final String? assignmentInfoJson; - final String rawJson; + final String accountId; + final String hrefKey; + final String requestUri; + final String displayName; + final String? description; + final String resourceTypesJson; + final int supportedComponentMask; + final String supportedCalendarDataJson; + final String supportedReportsJson; + final String currentUserPrivilegesJson; + final String? ownerHref; + final String? safeDisplayMetadataJson; + final String? color; + final int? sortOrder; + final String? calendarTimeZone; + final String? calendarTimeZoneId; + final String? scheduleTransparency; + final int? maximumResourceSize; + final int? maximumInstances; + final String? syncToken; + final String? ctag; + final bool readOnly; + final bool eventProjectionEnabled; + final bool taskProjectionEnabled; + final bool eventsSelected; + final bool tasksSelected; final bool serverMissing; - final bool localDirty; - final bool pendingDelete; - final bool pendingMove; - final bool localCreated; - final String? syncBaseUpdatedUtc; - final String? lastSyncedAtUtc; - final String createdLocalAtUtc; - final String updatedLocalAtUtc; - const Task({ - required this.accountId, - required this.taskListId, + final bool deleted; + final String? lastInventoryAtUtc; + final String? lastSyncAtUtc; + final int parserVersion; + final int projectionVersion; + final String createdAtUtc; + final String updatedAtUtc; + const DavCollection({ required this.id, - this.kind, - this.etag, - required this.title, - this.updatedUtc, - this.selfLink, - this.parent, - this.position, - this.notes, - this.status, - this.dueUtc, - this.completedUtc, - this.providerStatus, - this.bodyContent, - this.bodyContentType, - this.microsoftDueDateTime, - this.microsoftDueTimeZone, - this.microsoftStartDateTime, - this.microsoftStartTimeZone, - this.microsoftReminderDateTime, - this.microsoftReminderTimeZone, - this.microsoftIsReminderOn, - this.microsoftCompletedDateTime, - this.microsoftCompletedTimeZone, - this.recurrenceJson, - this.importance, - this.categoriesJson, - this.hasAttachments, - this.providerMetadataJson, - this.deleted, - this.hidden, - this.linksJson, - this.webViewLink, - this.assignmentInfoJson, - required this.rawJson, + required this.accountId, + required this.hrefKey, + required this.requestUri, + required this.displayName, + this.description, + required this.resourceTypesJson, + required this.supportedComponentMask, + required this.supportedCalendarDataJson, + required this.supportedReportsJson, + required this.currentUserPrivilegesJson, + this.ownerHref, + this.safeDisplayMetadataJson, + this.color, + this.sortOrder, + this.calendarTimeZone, + this.calendarTimeZoneId, + this.scheduleTransparency, + this.maximumResourceSize, + this.maximumInstances, + this.syncToken, + this.ctag, + required this.readOnly, + required this.eventProjectionEnabled, + required this.taskProjectionEnabled, + required this.eventsSelected, + required this.tasksSelected, required this.serverMissing, - required this.localDirty, - required this.pendingDelete, - required this.pendingMove, - required this.localCreated, - this.syncBaseUpdatedUtc, - this.lastSyncedAtUtc, - required this.createdLocalAtUtc, - required this.updatedLocalAtUtc, + required this.deleted, + this.lastInventoryAtUtc, + this.lastSyncAtUtc, + required this.parserVersion, + required this.projectionVersion, + required this.createdAtUtc, + required this.updatedAtUtc, }); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['account_id'] = Variable(accountId); - map['task_list_id'] = Variable(taskListId); map['id'] = Variable(id); - if (!nullToAbsent || kind != null) { - map['kind'] = Variable(kind); + map['account_id'] = Variable(accountId); + map['href_key'] = Variable(hrefKey); + map['request_uri'] = Variable(requestUri); + map['display_name'] = Variable(displayName); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); } - if (!nullToAbsent || etag != null) { - map['etag'] = Variable(etag); + map['resource_types_json'] = Variable(resourceTypesJson); + map['supported_component_mask'] = Variable(supportedComponentMask); + map['supported_calendar_data_json'] = Variable( + supportedCalendarDataJson, + ); + map['supported_reports_json'] = Variable(supportedReportsJson); + map['current_user_privileges_json'] = Variable( + currentUserPrivilegesJson, + ); + if (!nullToAbsent || ownerHref != null) { + map['owner_href'] = Variable(ownerHref); } - map['title'] = Variable(title); - if (!nullToAbsent || updatedUtc != null) { - map['updated_utc'] = Variable(updatedUtc); + if (!nullToAbsent || safeDisplayMetadataJson != null) { + map['safe_display_metadata_json'] = Variable( + safeDisplayMetadataJson, + ); } - if (!nullToAbsent || selfLink != null) { - map['self_link'] = Variable(selfLink); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); } - if (!nullToAbsent || parent != null) { - map['parent'] = Variable(parent); + if (!nullToAbsent || sortOrder != null) { + map['sort_order'] = Variable(sortOrder); } - if (!nullToAbsent || position != null) { - map['position'] = Variable(position); + if (!nullToAbsent || calendarTimeZone != null) { + map['calendar_time_zone'] = Variable(calendarTimeZone); } - if (!nullToAbsent || notes != null) { - map['notes'] = Variable(notes); + if (!nullToAbsent || calendarTimeZoneId != null) { + map['calendar_time_zone_id'] = Variable(calendarTimeZoneId); } - if (!nullToAbsent || status != null) { - map['status'] = Variable(status); + if (!nullToAbsent || scheduleTransparency != null) { + map['schedule_transparency'] = Variable(scheduleTransparency); } - if (!nullToAbsent || dueUtc != null) { - map['due_utc'] = Variable(dueUtc); + if (!nullToAbsent || maximumResourceSize != null) { + map['maximum_resource_size'] = Variable(maximumResourceSize); } - if (!nullToAbsent || completedUtc != null) { - map['completed_utc'] = Variable(completedUtc); + if (!nullToAbsent || maximumInstances != null) { + map['maximum_instances'] = Variable(maximumInstances); } - if (!nullToAbsent || providerStatus != null) { - map['provider_status'] = Variable(providerStatus); - } - if (!nullToAbsent || bodyContent != null) { - map['body_content'] = Variable(bodyContent); - } - if (!nullToAbsent || bodyContentType != null) { - map['body_content_type'] = Variable(bodyContentType); - } - if (!nullToAbsent || microsoftDueDateTime != null) { - map['microsoft_due_date_time'] = Variable(microsoftDueDateTime); - } - if (!nullToAbsent || microsoftDueTimeZone != null) { - map['microsoft_due_time_zone'] = Variable(microsoftDueTimeZone); - } - if (!nullToAbsent || microsoftStartDateTime != null) { - map['microsoft_start_date_time'] = Variable( - microsoftStartDateTime, - ); - } - if (!nullToAbsent || microsoftStartTimeZone != null) { - map['microsoft_start_time_zone'] = Variable( - microsoftStartTimeZone, - ); - } - if (!nullToAbsent || microsoftReminderDateTime != null) { - map['microsoft_reminder_date_time'] = Variable( - microsoftReminderDateTime, - ); - } - if (!nullToAbsent || microsoftReminderTimeZone != null) { - map['microsoft_reminder_time_zone'] = Variable( - microsoftReminderTimeZone, - ); - } - if (!nullToAbsent || microsoftIsReminderOn != null) { - map['microsoft_is_reminder_on'] = Variable(microsoftIsReminderOn); - } - if (!nullToAbsent || microsoftCompletedDateTime != null) { - map['microsoft_completed_date_time'] = Variable( - microsoftCompletedDateTime, - ); - } - if (!nullToAbsent || microsoftCompletedTimeZone != null) { - map['microsoft_completed_time_zone'] = Variable( - microsoftCompletedTimeZone, - ); - } - if (!nullToAbsent || recurrenceJson != null) { - map['recurrence_json'] = Variable(recurrenceJson); - } - if (!nullToAbsent || importance != null) { - map['importance'] = Variable(importance); - } - if (!nullToAbsent || categoriesJson != null) { - map['categories_json'] = Variable(categoriesJson); - } - if (!nullToAbsent || hasAttachments != null) { - map['has_attachments'] = Variable(hasAttachments); - } - if (!nullToAbsent || providerMetadataJson != null) { - map['provider_metadata_json'] = Variable(providerMetadataJson); - } - if (!nullToAbsent || deleted != null) { - map['deleted'] = Variable(deleted); - } - if (!nullToAbsent || hidden != null) { - map['hidden'] = Variable(hidden); - } - if (!nullToAbsent || linksJson != null) { - map['links_json'] = Variable(linksJson); - } - if (!nullToAbsent || webViewLink != null) { - map['web_view_link'] = Variable(webViewLink); + if (!nullToAbsent || syncToken != null) { + map['sync_token'] = Variable(syncToken); } - if (!nullToAbsent || assignmentInfoJson != null) { - map['assignment_info_json'] = Variable(assignmentInfoJson); + if (!nullToAbsent || ctag != null) { + map['ctag'] = Variable(ctag); } - map['raw_json'] = Variable(rawJson); + map['read_only'] = Variable(readOnly); + map['event_projection_enabled'] = Variable(eventProjectionEnabled); + map['task_projection_enabled'] = Variable(taskProjectionEnabled); + map['events_selected'] = Variable(eventsSelected); + map['tasks_selected'] = Variable(tasksSelected); map['server_missing'] = Variable(serverMissing); - map['local_dirty'] = Variable(localDirty); - map['pending_delete'] = Variable(pendingDelete); - map['pending_move'] = Variable(pendingMove); - map['local_created'] = Variable(localCreated); - if (!nullToAbsent || syncBaseUpdatedUtc != null) { - map['sync_base_updated_utc'] = Variable(syncBaseUpdatedUtc); + map['deleted'] = Variable(deleted); + if (!nullToAbsent || lastInventoryAtUtc != null) { + map['last_inventory_at_utc'] = Variable(lastInventoryAtUtc); } - if (!nullToAbsent || lastSyncedAtUtc != null) { - map['last_synced_at_utc'] = Variable(lastSyncedAtUtc); + if (!nullToAbsent || lastSyncAtUtc != null) { + map['last_sync_at_utc'] = Variable(lastSyncAtUtc); } - map['created_local_at_utc'] = Variable(createdLocalAtUtc); - map['updated_local_at_utc'] = Variable(updatedLocalAtUtc); + map['parser_version'] = Variable(parserVersion); + map['projection_version'] = Variable(projectionVersion); + map['created_at_utc'] = Variable(createdAtUtc); + map['updated_at_utc'] = Variable(updatedAtUtc); return map; } - TasksCompanion toCompanion(bool nullToAbsent) { - return TasksCompanion( - accountId: Value(accountId), - taskListId: Value(taskListId), + DavCollectionsCompanion toCompanion(bool nullToAbsent) { + return DavCollectionsCompanion( id: Value(id), - kind: kind == null && nullToAbsent ? const Value.absent() : Value(kind), - etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag), - title: Value(title), - updatedUtc: updatedUtc == null && nullToAbsent - ? const Value.absent() - : Value(updatedUtc), - selfLink: selfLink == null && nullToAbsent - ? const Value.absent() - : Value(selfLink), - parent: parent == null && nullToAbsent - ? const Value.absent() - : Value(parent), - position: position == null && nullToAbsent - ? const Value.absent() - : Value(position), - notes: notes == null && nullToAbsent - ? const Value.absent() - : Value(notes), - status: status == null && nullToAbsent - ? const Value.absent() - : Value(status), - dueUtc: dueUtc == null && nullToAbsent - ? const Value.absent() - : Value(dueUtc), - completedUtc: completedUtc == null && nullToAbsent - ? const Value.absent() - : Value(completedUtc), - providerStatus: providerStatus == null && nullToAbsent - ? const Value.absent() - : Value(providerStatus), - bodyContent: bodyContent == null && nullToAbsent - ? const Value.absent() - : Value(bodyContent), - bodyContentType: bodyContentType == null && nullToAbsent - ? const Value.absent() - : Value(bodyContentType), - microsoftDueDateTime: microsoftDueDateTime == null && nullToAbsent - ? const Value.absent() - : Value(microsoftDueDateTime), - microsoftDueTimeZone: microsoftDueTimeZone == null && nullToAbsent - ? const Value.absent() - : Value(microsoftDueTimeZone), - microsoftStartDateTime: microsoftStartDateTime == null && nullToAbsent - ? const Value.absent() - : Value(microsoftStartDateTime), - microsoftStartTimeZone: microsoftStartTimeZone == null && nullToAbsent - ? const Value.absent() - : Value(microsoftStartTimeZone), - microsoftReminderDateTime: - microsoftReminderDateTime == null && nullToAbsent - ? const Value.absent() - : Value(microsoftReminderDateTime), - microsoftReminderTimeZone: - microsoftReminderTimeZone == null && nullToAbsent - ? const Value.absent() - : Value(microsoftReminderTimeZone), - microsoftIsReminderOn: microsoftIsReminderOn == null && nullToAbsent - ? const Value.absent() - : Value(microsoftIsReminderOn), - microsoftCompletedDateTime: - microsoftCompletedDateTime == null && nullToAbsent - ? const Value.absent() - : Value(microsoftCompletedDateTime), - microsoftCompletedTimeZone: - microsoftCompletedTimeZone == null && nullToAbsent + accountId: Value(accountId), + hrefKey: Value(hrefKey), + requestUri: Value(requestUri), + displayName: Value(displayName), + description: description == null && nullToAbsent ? const Value.absent() - : Value(microsoftCompletedTimeZone), - recurrenceJson: recurrenceJson == null && nullToAbsent + : Value(description), + resourceTypesJson: Value(resourceTypesJson), + supportedComponentMask: Value(supportedComponentMask), + supportedCalendarDataJson: Value(supportedCalendarDataJson), + supportedReportsJson: Value(supportedReportsJson), + currentUserPrivilegesJson: Value(currentUserPrivilegesJson), + ownerHref: ownerHref == null && nullToAbsent ? const Value.absent() - : Value(recurrenceJson), - importance: importance == null && nullToAbsent + : Value(ownerHref), + safeDisplayMetadataJson: safeDisplayMetadataJson == null && nullToAbsent ? const Value.absent() - : Value(importance), - categoriesJson: categoriesJson == null && nullToAbsent + : Value(safeDisplayMetadataJson), + color: color == null && nullToAbsent ? const Value.absent() - : Value(categoriesJson), - hasAttachments: hasAttachments == null && nullToAbsent + : Value(color), + sortOrder: sortOrder == null && nullToAbsent ? const Value.absent() - : Value(hasAttachments), - providerMetadataJson: providerMetadataJson == null && nullToAbsent + : Value(sortOrder), + calendarTimeZone: calendarTimeZone == null && nullToAbsent ? const Value.absent() - : Value(providerMetadataJson), - deleted: deleted == null && nullToAbsent + : Value(calendarTimeZone), + calendarTimeZoneId: calendarTimeZoneId == null && nullToAbsent ? const Value.absent() - : Value(deleted), - hidden: hidden == null && nullToAbsent + : Value(calendarTimeZoneId), + scheduleTransparency: scheduleTransparency == null && nullToAbsent ? const Value.absent() - : Value(hidden), - linksJson: linksJson == null && nullToAbsent + : Value(scheduleTransparency), + maximumResourceSize: maximumResourceSize == null && nullToAbsent ? const Value.absent() - : Value(linksJson), - webViewLink: webViewLink == null && nullToAbsent + : Value(maximumResourceSize), + maximumInstances: maximumInstances == null && nullToAbsent ? const Value.absent() - : Value(webViewLink), - assignmentInfoJson: assignmentInfoJson == null && nullToAbsent + : Value(maximumInstances), + syncToken: syncToken == null && nullToAbsent ? const Value.absent() - : Value(assignmentInfoJson), - rawJson: Value(rawJson), + : Value(syncToken), + ctag: ctag == null && nullToAbsent ? const Value.absent() : Value(ctag), + readOnly: Value(readOnly), + eventProjectionEnabled: Value(eventProjectionEnabled), + taskProjectionEnabled: Value(taskProjectionEnabled), + eventsSelected: Value(eventsSelected), + tasksSelected: Value(tasksSelected), serverMissing: Value(serverMissing), - localDirty: Value(localDirty), - pendingDelete: Value(pendingDelete), - pendingMove: Value(pendingMove), - localCreated: Value(localCreated), - syncBaseUpdatedUtc: syncBaseUpdatedUtc == null && nullToAbsent + deleted: Value(deleted), + lastInventoryAtUtc: lastInventoryAtUtc == null && nullToAbsent ? const Value.absent() - : Value(syncBaseUpdatedUtc), - lastSyncedAtUtc: lastSyncedAtUtc == null && nullToAbsent + : Value(lastInventoryAtUtc), + lastSyncAtUtc: lastSyncAtUtc == null && nullToAbsent ? const Value.absent() - : Value(lastSyncedAtUtc), - createdLocalAtUtc: Value(createdLocalAtUtc), - updatedLocalAtUtc: Value(updatedLocalAtUtc), + : Value(lastSyncAtUtc), + parserVersion: Value(parserVersion), + projectionVersion: Value(projectionVersion), + createdAtUtc: Value(createdAtUtc), + updatedAtUtc: Value(updatedAtUtc), ); } - factory Task.fromJson( + factory DavCollection.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return Task( - accountId: serializer.fromJson(json['accountId']), - taskListId: serializer.fromJson(json['taskListId']), + return DavCollection( id: serializer.fromJson(json['id']), - kind: serializer.fromJson(json['kind']), - etag: serializer.fromJson(json['etag']), - title: serializer.fromJson(json['title']), - updatedUtc: serializer.fromJson(json['updatedUtc']), - selfLink: serializer.fromJson(json['selfLink']), - parent: serializer.fromJson(json['parent']), - position: serializer.fromJson(json['position']), - notes: serializer.fromJson(json['notes']), - status: serializer.fromJson(json['status']), - dueUtc: serializer.fromJson(json['dueUtc']), - completedUtc: serializer.fromJson(json['completedUtc']), - providerStatus: serializer.fromJson(json['providerStatus']), - bodyContent: serializer.fromJson(json['bodyContent']), - bodyContentType: serializer.fromJson(json['bodyContentType']), - microsoftDueDateTime: serializer.fromJson( - json['microsoftDueDateTime'], - ), - microsoftDueTimeZone: serializer.fromJson( - json['microsoftDueTimeZone'], + accountId: serializer.fromJson(json['accountId']), + hrefKey: serializer.fromJson(json['hrefKey']), + requestUri: serializer.fromJson(json['requestUri']), + displayName: serializer.fromJson(json['displayName']), + description: serializer.fromJson(json['description']), + resourceTypesJson: serializer.fromJson(json['resourceTypesJson']), + supportedComponentMask: serializer.fromJson( + json['supportedComponentMask'], ), - microsoftStartDateTime: serializer.fromJson( - json['microsoftStartDateTime'], + supportedCalendarDataJson: serializer.fromJson( + json['supportedCalendarDataJson'], ), - microsoftStartTimeZone: serializer.fromJson( - json['microsoftStartTimeZone'], + supportedReportsJson: serializer.fromJson( + json['supportedReportsJson'], ), - microsoftReminderDateTime: serializer.fromJson( - json['microsoftReminderDateTime'], + currentUserPrivilegesJson: serializer.fromJson( + json['currentUserPrivilegesJson'], ), - microsoftReminderTimeZone: serializer.fromJson( - json['microsoftReminderTimeZone'], + ownerHref: serializer.fromJson(json['ownerHref']), + safeDisplayMetadataJson: serializer.fromJson( + json['safeDisplayMetadataJson'], ), - microsoftIsReminderOn: serializer.fromJson( - json['microsoftIsReminderOn'], + color: serializer.fromJson(json['color']), + sortOrder: serializer.fromJson(json['sortOrder']), + calendarTimeZone: serializer.fromJson(json['calendarTimeZone']), + calendarTimeZoneId: serializer.fromJson( + json['calendarTimeZoneId'], ), - microsoftCompletedDateTime: serializer.fromJson( - json['microsoftCompletedDateTime'], + scheduleTransparency: serializer.fromJson( + json['scheduleTransparency'], ), - microsoftCompletedTimeZone: serializer.fromJson( - json['microsoftCompletedTimeZone'], + maximumResourceSize: serializer.fromJson( + json['maximumResourceSize'], ), - recurrenceJson: serializer.fromJson(json['recurrenceJson']), - importance: serializer.fromJson(json['importance']), - categoriesJson: serializer.fromJson(json['categoriesJson']), - hasAttachments: serializer.fromJson(json['hasAttachments']), - providerMetadataJson: serializer.fromJson( - json['providerMetadataJson'], + maximumInstances: serializer.fromJson(json['maximumInstances']), + syncToken: serializer.fromJson(json['syncToken']), + ctag: serializer.fromJson(json['ctag']), + readOnly: serializer.fromJson(json['readOnly']), + eventProjectionEnabled: serializer.fromJson( + json['eventProjectionEnabled'], ), - deleted: serializer.fromJson(json['deleted']), - hidden: serializer.fromJson(json['hidden']), - linksJson: serializer.fromJson(json['linksJson']), - webViewLink: serializer.fromJson(json['webViewLink']), - assignmentInfoJson: serializer.fromJson( - json['assignmentInfoJson'], + taskProjectionEnabled: serializer.fromJson( + json['taskProjectionEnabled'], ), - rawJson: serializer.fromJson(json['rawJson']), + eventsSelected: serializer.fromJson(json['eventsSelected']), + tasksSelected: serializer.fromJson(json['tasksSelected']), serverMissing: serializer.fromJson(json['serverMissing']), - localDirty: serializer.fromJson(json['localDirty']), - pendingDelete: serializer.fromJson(json['pendingDelete']), - pendingMove: serializer.fromJson(json['pendingMove']), - localCreated: serializer.fromJson(json['localCreated']), - syncBaseUpdatedUtc: serializer.fromJson( - json['syncBaseUpdatedUtc'], + deleted: serializer.fromJson(json['deleted']), + lastInventoryAtUtc: serializer.fromJson( + json['lastInventoryAtUtc'], ), - lastSyncedAtUtc: serializer.fromJson(json['lastSyncedAtUtc']), - createdLocalAtUtc: serializer.fromJson(json['createdLocalAtUtc']), - updatedLocalAtUtc: serializer.fromJson(json['updatedLocalAtUtc']), + lastSyncAtUtc: serializer.fromJson(json['lastSyncAtUtc']), + parserVersion: serializer.fromJson(json['parserVersion']), + projectionVersion: serializer.fromJson(json['projectionVersion']), + createdAtUtc: serializer.fromJson(json['createdAtUtc']), + updatedAtUtc: serializer.fromJson(json['updatedAtUtc']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'accountId': serializer.toJson(accountId), - 'taskListId': serializer.toJson(taskListId), 'id': serializer.toJson(id), - 'kind': serializer.toJson(kind), - 'etag': serializer.toJson(etag), - 'title': serializer.toJson(title), - 'updatedUtc': serializer.toJson(updatedUtc), - 'selfLink': serializer.toJson(selfLink), - 'parent': serializer.toJson(parent), - 'position': serializer.toJson(position), - 'notes': serializer.toJson(notes), - 'status': serializer.toJson(status), - 'dueUtc': serializer.toJson(dueUtc), - 'completedUtc': serializer.toJson(completedUtc), - 'providerStatus': serializer.toJson(providerStatus), - 'bodyContent': serializer.toJson(bodyContent), - 'bodyContentType': serializer.toJson(bodyContentType), - 'microsoftDueDateTime': serializer.toJson(microsoftDueDateTime), - 'microsoftDueTimeZone': serializer.toJson(microsoftDueTimeZone), - 'microsoftStartDateTime': serializer.toJson( - microsoftStartDateTime, - ), - 'microsoftStartTimeZone': serializer.toJson( - microsoftStartTimeZone, - ), - 'microsoftReminderDateTime': serializer.toJson( - microsoftReminderDateTime, - ), - 'microsoftReminderTimeZone': serializer.toJson( - microsoftReminderTimeZone, + 'accountId': serializer.toJson(accountId), + 'hrefKey': serializer.toJson(hrefKey), + 'requestUri': serializer.toJson(requestUri), + 'displayName': serializer.toJson(displayName), + 'description': serializer.toJson(description), + 'resourceTypesJson': serializer.toJson(resourceTypesJson), + 'supportedComponentMask': serializer.toJson(supportedComponentMask), + 'supportedCalendarDataJson': serializer.toJson( + supportedCalendarDataJson, ), - 'microsoftIsReminderOn': serializer.toJson(microsoftIsReminderOn), - 'microsoftCompletedDateTime': serializer.toJson( - microsoftCompletedDateTime, + 'supportedReportsJson': serializer.toJson(supportedReportsJson), + 'currentUserPrivilegesJson': serializer.toJson( + currentUserPrivilegesJson, ), - 'microsoftCompletedTimeZone': serializer.toJson( - microsoftCompletedTimeZone, + 'ownerHref': serializer.toJson(ownerHref), + 'safeDisplayMetadataJson': serializer.toJson( + safeDisplayMetadataJson, ), - 'recurrenceJson': serializer.toJson(recurrenceJson), - 'importance': serializer.toJson(importance), - 'categoriesJson': serializer.toJson(categoriesJson), - 'hasAttachments': serializer.toJson(hasAttachments), - 'providerMetadataJson': serializer.toJson(providerMetadataJson), - 'deleted': serializer.toJson(deleted), - 'hidden': serializer.toJson(hidden), - 'linksJson': serializer.toJson(linksJson), - 'webViewLink': serializer.toJson(webViewLink), - 'assignmentInfoJson': serializer.toJson(assignmentInfoJson), - 'rawJson': serializer.toJson(rawJson), + 'color': serializer.toJson(color), + 'sortOrder': serializer.toJson(sortOrder), + 'calendarTimeZone': serializer.toJson(calendarTimeZone), + 'calendarTimeZoneId': serializer.toJson(calendarTimeZoneId), + 'scheduleTransparency': serializer.toJson(scheduleTransparency), + 'maximumResourceSize': serializer.toJson(maximumResourceSize), + 'maximumInstances': serializer.toJson(maximumInstances), + 'syncToken': serializer.toJson(syncToken), + 'ctag': serializer.toJson(ctag), + 'readOnly': serializer.toJson(readOnly), + 'eventProjectionEnabled': serializer.toJson(eventProjectionEnabled), + 'taskProjectionEnabled': serializer.toJson(taskProjectionEnabled), + 'eventsSelected': serializer.toJson(eventsSelected), + 'tasksSelected': serializer.toJson(tasksSelected), 'serverMissing': serializer.toJson(serverMissing), - 'localDirty': serializer.toJson(localDirty), - 'pendingDelete': serializer.toJson(pendingDelete), - 'pendingMove': serializer.toJson(pendingMove), - 'localCreated': serializer.toJson(localCreated), - 'syncBaseUpdatedUtc': serializer.toJson(syncBaseUpdatedUtc), - 'lastSyncedAtUtc': serializer.toJson(lastSyncedAtUtc), - 'createdLocalAtUtc': serializer.toJson(createdLocalAtUtc), - 'updatedLocalAtUtc': serializer.toJson(updatedLocalAtUtc), + 'deleted': serializer.toJson(deleted), + 'lastInventoryAtUtc': serializer.toJson(lastInventoryAtUtc), + 'lastSyncAtUtc': serializer.toJson(lastSyncAtUtc), + 'parserVersion': serializer.toJson(parserVersion), + 'projectionVersion': serializer.toJson(projectionVersion), + 'createdAtUtc': serializer.toJson(createdAtUtc), + 'updatedAtUtc': serializer.toJson(updatedAtUtc), }; } - Task copyWith({ - String? accountId, - String? taskListId, + DavCollection copyWith({ String? id, - Value kind = const Value.absent(), - Value etag = const Value.absent(), - String? title, - Value updatedUtc = const Value.absent(), - Value selfLink = const Value.absent(), - Value parent = const Value.absent(), - Value position = const Value.absent(), - Value notes = const Value.absent(), - Value status = const Value.absent(), - Value dueUtc = const Value.absent(), - Value completedUtc = const Value.absent(), - Value providerStatus = const Value.absent(), - Value bodyContent = const Value.absent(), - Value bodyContentType = const Value.absent(), - Value microsoftDueDateTime = const Value.absent(), - Value microsoftDueTimeZone = const Value.absent(), - Value microsoftStartDateTime = const Value.absent(), - Value microsoftStartTimeZone = const Value.absent(), - Value microsoftReminderDateTime = const Value.absent(), - Value microsoftReminderTimeZone = const Value.absent(), - Value microsoftIsReminderOn = const Value.absent(), - Value microsoftCompletedDateTime = const Value.absent(), - Value microsoftCompletedTimeZone = const Value.absent(), - Value recurrenceJson = const Value.absent(), - Value importance = const Value.absent(), - Value categoriesJson = const Value.absent(), - Value hasAttachments = const Value.absent(), - Value providerMetadataJson = const Value.absent(), - Value deleted = const Value.absent(), - Value hidden = const Value.absent(), - Value linksJson = const Value.absent(), - Value webViewLink = const Value.absent(), - Value assignmentInfoJson = const Value.absent(), - String? rawJson, + String? accountId, + String? hrefKey, + String? requestUri, + String? displayName, + Value description = const Value.absent(), + String? resourceTypesJson, + int? supportedComponentMask, + String? supportedCalendarDataJson, + String? supportedReportsJson, + String? currentUserPrivilegesJson, + Value ownerHref = const Value.absent(), + Value safeDisplayMetadataJson = const Value.absent(), + Value color = const Value.absent(), + Value sortOrder = const Value.absent(), + Value calendarTimeZone = const Value.absent(), + Value calendarTimeZoneId = const Value.absent(), + Value scheduleTransparency = const Value.absent(), + Value maximumResourceSize = const Value.absent(), + Value maximumInstances = const Value.absent(), + Value syncToken = const Value.absent(), + Value ctag = const Value.absent(), + bool? readOnly, + bool? eventProjectionEnabled, + bool? taskProjectionEnabled, + bool? eventsSelected, + bool? tasksSelected, bool? serverMissing, - bool? localDirty, - bool? pendingDelete, - bool? pendingMove, - bool? localCreated, - Value syncBaseUpdatedUtc = const Value.absent(), - Value lastSyncedAtUtc = const Value.absent(), - String? createdLocalAtUtc, - String? updatedLocalAtUtc, - }) => Task( - accountId: accountId ?? this.accountId, - taskListId: taskListId ?? this.taskListId, + bool? deleted, + Value lastInventoryAtUtc = const Value.absent(), + Value lastSyncAtUtc = const Value.absent(), + int? parserVersion, + int? projectionVersion, + String? createdAtUtc, + String? updatedAtUtc, + }) => DavCollection( id: id ?? this.id, - kind: kind.present ? kind.value : this.kind, - etag: etag.present ? etag.value : this.etag, - title: title ?? this.title, - updatedUtc: updatedUtc.present ? updatedUtc.value : this.updatedUtc, - selfLink: selfLink.present ? selfLink.value : this.selfLink, - parent: parent.present ? parent.value : this.parent, - position: position.present ? position.value : this.position, - notes: notes.present ? notes.value : this.notes, - status: status.present ? status.value : this.status, - dueUtc: dueUtc.present ? dueUtc.value : this.dueUtc, - completedUtc: completedUtc.present ? completedUtc.value : this.completedUtc, - providerStatus: providerStatus.present - ? providerStatus.value - : this.providerStatus, - bodyContent: bodyContent.present ? bodyContent.value : this.bodyContent, - bodyContentType: bodyContentType.present - ? bodyContentType.value - : this.bodyContentType, - microsoftDueDateTime: microsoftDueDateTime.present - ? microsoftDueDateTime.value - : this.microsoftDueDateTime, - microsoftDueTimeZone: microsoftDueTimeZone.present - ? microsoftDueTimeZone.value - : this.microsoftDueTimeZone, - microsoftStartDateTime: microsoftStartDateTime.present - ? microsoftStartDateTime.value - : this.microsoftStartDateTime, - microsoftStartTimeZone: microsoftStartTimeZone.present - ? microsoftStartTimeZone.value - : this.microsoftStartTimeZone, - microsoftReminderDateTime: microsoftReminderDateTime.present - ? microsoftReminderDateTime.value - : this.microsoftReminderDateTime, - microsoftReminderTimeZone: microsoftReminderTimeZone.present - ? microsoftReminderTimeZone.value - : this.microsoftReminderTimeZone, - microsoftIsReminderOn: microsoftIsReminderOn.present - ? microsoftIsReminderOn.value - : this.microsoftIsReminderOn, - microsoftCompletedDateTime: microsoftCompletedDateTime.present - ? microsoftCompletedDateTime.value - : this.microsoftCompletedDateTime, - microsoftCompletedTimeZone: microsoftCompletedTimeZone.present - ? microsoftCompletedTimeZone.value - : this.microsoftCompletedTimeZone, - recurrenceJson: recurrenceJson.present - ? recurrenceJson.value - : this.recurrenceJson, - importance: importance.present ? importance.value : this.importance, - categoriesJson: categoriesJson.present - ? categoriesJson.value - : this.categoriesJson, - hasAttachments: hasAttachments.present - ? hasAttachments.value - : this.hasAttachments, - providerMetadataJson: providerMetadataJson.present - ? providerMetadataJson.value - : this.providerMetadataJson, - deleted: deleted.present ? deleted.value : this.deleted, - hidden: hidden.present ? hidden.value : this.hidden, - linksJson: linksJson.present ? linksJson.value : this.linksJson, - webViewLink: webViewLink.present ? webViewLink.value : this.webViewLink, - assignmentInfoJson: assignmentInfoJson.present - ? assignmentInfoJson.value - : this.assignmentInfoJson, - rawJson: rawJson ?? this.rawJson, + accountId: accountId ?? this.accountId, + hrefKey: hrefKey ?? this.hrefKey, + requestUri: requestUri ?? this.requestUri, + displayName: displayName ?? this.displayName, + description: description.present ? description.value : this.description, + resourceTypesJson: resourceTypesJson ?? this.resourceTypesJson, + supportedComponentMask: + supportedComponentMask ?? this.supportedComponentMask, + supportedCalendarDataJson: + supportedCalendarDataJson ?? this.supportedCalendarDataJson, + supportedReportsJson: supportedReportsJson ?? this.supportedReportsJson, + currentUserPrivilegesJson: + currentUserPrivilegesJson ?? this.currentUserPrivilegesJson, + ownerHref: ownerHref.present ? ownerHref.value : this.ownerHref, + safeDisplayMetadataJson: safeDisplayMetadataJson.present + ? safeDisplayMetadataJson.value + : this.safeDisplayMetadataJson, + color: color.present ? color.value : this.color, + sortOrder: sortOrder.present ? sortOrder.value : this.sortOrder, + calendarTimeZone: calendarTimeZone.present + ? calendarTimeZone.value + : this.calendarTimeZone, + calendarTimeZoneId: calendarTimeZoneId.present + ? calendarTimeZoneId.value + : this.calendarTimeZoneId, + scheduleTransparency: scheduleTransparency.present + ? scheduleTransparency.value + : this.scheduleTransparency, + maximumResourceSize: maximumResourceSize.present + ? maximumResourceSize.value + : this.maximumResourceSize, + maximumInstances: maximumInstances.present + ? maximumInstances.value + : this.maximumInstances, + syncToken: syncToken.present ? syncToken.value : this.syncToken, + ctag: ctag.present ? ctag.value : this.ctag, + readOnly: readOnly ?? this.readOnly, + eventProjectionEnabled: + eventProjectionEnabled ?? this.eventProjectionEnabled, + taskProjectionEnabled: taskProjectionEnabled ?? this.taskProjectionEnabled, + eventsSelected: eventsSelected ?? this.eventsSelected, + tasksSelected: tasksSelected ?? this.tasksSelected, serverMissing: serverMissing ?? this.serverMissing, - localDirty: localDirty ?? this.localDirty, - pendingDelete: pendingDelete ?? this.pendingDelete, - pendingMove: pendingMove ?? this.pendingMove, - localCreated: localCreated ?? this.localCreated, - syncBaseUpdatedUtc: syncBaseUpdatedUtc.present - ? syncBaseUpdatedUtc.value - : this.syncBaseUpdatedUtc, - lastSyncedAtUtc: lastSyncedAtUtc.present - ? lastSyncedAtUtc.value - : this.lastSyncedAtUtc, - createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, - updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, + deleted: deleted ?? this.deleted, + lastInventoryAtUtc: lastInventoryAtUtc.present + ? lastInventoryAtUtc.value + : this.lastInventoryAtUtc, + lastSyncAtUtc: lastSyncAtUtc.present + ? lastSyncAtUtc.value + : this.lastSyncAtUtc, + parserVersion: parserVersion ?? this.parserVersion, + projectionVersion: projectionVersion ?? this.projectionVersion, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, ); - Task copyWithCompanion(TasksCompanion data) { - return Task( - accountId: data.accountId.present ? data.accountId.value : this.accountId, - taskListId: data.taskListId.present - ? data.taskListId.value - : this.taskListId, + DavCollection copyWithCompanion(DavCollectionsCompanion data) { + return DavCollection( id: data.id.present ? data.id.value : this.id, - kind: data.kind.present ? data.kind.value : this.kind, - etag: data.etag.present ? data.etag.value : this.etag, - title: data.title.present ? data.title.value : this.title, - updatedUtc: data.updatedUtc.present - ? data.updatedUtc.value - : this.updatedUtc, - selfLink: data.selfLink.present ? data.selfLink.value : this.selfLink, - parent: data.parent.present ? data.parent.value : this.parent, - position: data.position.present ? data.position.value : this.position, - notes: data.notes.present ? data.notes.value : this.notes, - status: data.status.present ? data.status.value : this.status, - dueUtc: data.dueUtc.present ? data.dueUtc.value : this.dueUtc, - completedUtc: data.completedUtc.present - ? data.completedUtc.value - : this.completedUtc, - providerStatus: data.providerStatus.present - ? data.providerStatus.value - : this.providerStatus, - bodyContent: data.bodyContent.present - ? data.bodyContent.value - : this.bodyContent, - bodyContentType: data.bodyContentType.present - ? data.bodyContentType.value - : this.bodyContentType, - microsoftDueDateTime: data.microsoftDueDateTime.present - ? data.microsoftDueDateTime.value - : this.microsoftDueDateTime, - microsoftDueTimeZone: data.microsoftDueTimeZone.present - ? data.microsoftDueTimeZone.value - : this.microsoftDueTimeZone, - microsoftStartDateTime: data.microsoftStartDateTime.present - ? data.microsoftStartDateTime.value - : this.microsoftStartDateTime, - microsoftStartTimeZone: data.microsoftStartTimeZone.present - ? data.microsoftStartTimeZone.value - : this.microsoftStartTimeZone, - microsoftReminderDateTime: data.microsoftReminderDateTime.present - ? data.microsoftReminderDateTime.value - : this.microsoftReminderDateTime, - microsoftReminderTimeZone: data.microsoftReminderTimeZone.present - ? data.microsoftReminderTimeZone.value - : this.microsoftReminderTimeZone, - microsoftIsReminderOn: data.microsoftIsReminderOn.present - ? data.microsoftIsReminderOn.value - : this.microsoftIsReminderOn, - microsoftCompletedDateTime: data.microsoftCompletedDateTime.present - ? data.microsoftCompletedDateTime.value - : this.microsoftCompletedDateTime, - microsoftCompletedTimeZone: data.microsoftCompletedTimeZone.present - ? data.microsoftCompletedTimeZone.value - : this.microsoftCompletedTimeZone, - recurrenceJson: data.recurrenceJson.present - ? data.recurrenceJson.value - : this.recurrenceJson, - importance: data.importance.present - ? data.importance.value - : this.importance, - categoriesJson: data.categoriesJson.present - ? data.categoriesJson.value - : this.categoriesJson, - hasAttachments: data.hasAttachments.present - ? data.hasAttachments.value - : this.hasAttachments, - providerMetadataJson: data.providerMetadataJson.present - ? data.providerMetadataJson.value - : this.providerMetadataJson, - deleted: data.deleted.present ? data.deleted.value : this.deleted, - hidden: data.hidden.present ? data.hidden.value : this.hidden, - linksJson: data.linksJson.present ? data.linksJson.value : this.linksJson, - webViewLink: data.webViewLink.present - ? data.webViewLink.value - : this.webViewLink, - assignmentInfoJson: data.assignmentInfoJson.present - ? data.assignmentInfoJson.value - : this.assignmentInfoJson, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + hrefKey: data.hrefKey.present ? data.hrefKey.value : this.hrefKey, + requestUri: data.requestUri.present + ? data.requestUri.value + : this.requestUri, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + description: data.description.present + ? data.description.value + : this.description, + resourceTypesJson: data.resourceTypesJson.present + ? data.resourceTypesJson.value + : this.resourceTypesJson, + supportedComponentMask: data.supportedComponentMask.present + ? data.supportedComponentMask.value + : this.supportedComponentMask, + supportedCalendarDataJson: data.supportedCalendarDataJson.present + ? data.supportedCalendarDataJson.value + : this.supportedCalendarDataJson, + supportedReportsJson: data.supportedReportsJson.present + ? data.supportedReportsJson.value + : this.supportedReportsJson, + currentUserPrivilegesJson: data.currentUserPrivilegesJson.present + ? data.currentUserPrivilegesJson.value + : this.currentUserPrivilegesJson, + ownerHref: data.ownerHref.present ? data.ownerHref.value : this.ownerHref, + safeDisplayMetadataJson: data.safeDisplayMetadataJson.present + ? data.safeDisplayMetadataJson.value + : this.safeDisplayMetadataJson, + color: data.color.present ? data.color.value : this.color, + sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder, + calendarTimeZone: data.calendarTimeZone.present + ? data.calendarTimeZone.value + : this.calendarTimeZone, + calendarTimeZoneId: data.calendarTimeZoneId.present + ? data.calendarTimeZoneId.value + : this.calendarTimeZoneId, + scheduleTransparency: data.scheduleTransparency.present + ? data.scheduleTransparency.value + : this.scheduleTransparency, + maximumResourceSize: data.maximumResourceSize.present + ? data.maximumResourceSize.value + : this.maximumResourceSize, + maximumInstances: data.maximumInstances.present + ? data.maximumInstances.value + : this.maximumInstances, + syncToken: data.syncToken.present ? data.syncToken.value : this.syncToken, + ctag: data.ctag.present ? data.ctag.value : this.ctag, + readOnly: data.readOnly.present ? data.readOnly.value : this.readOnly, + eventProjectionEnabled: data.eventProjectionEnabled.present + ? data.eventProjectionEnabled.value + : this.eventProjectionEnabled, + taskProjectionEnabled: data.taskProjectionEnabled.present + ? data.taskProjectionEnabled.value + : this.taskProjectionEnabled, + eventsSelected: data.eventsSelected.present + ? data.eventsSelected.value + : this.eventsSelected, + tasksSelected: data.tasksSelected.present + ? data.tasksSelected.value + : this.tasksSelected, serverMissing: data.serverMissing.present ? data.serverMissing.value : this.serverMissing, - localDirty: data.localDirty.present - ? data.localDirty.value - : this.localDirty, - pendingDelete: data.pendingDelete.present - ? data.pendingDelete.value - : this.pendingDelete, - pendingMove: data.pendingMove.present - ? data.pendingMove.value - : this.pendingMove, - localCreated: data.localCreated.present - ? data.localCreated.value - : this.localCreated, - syncBaseUpdatedUtc: data.syncBaseUpdatedUtc.present - ? data.syncBaseUpdatedUtc.value - : this.syncBaseUpdatedUtc, - lastSyncedAtUtc: data.lastSyncedAtUtc.present - ? data.lastSyncedAtUtc.value - : this.lastSyncedAtUtc, - createdLocalAtUtc: data.createdLocalAtUtc.present - ? data.createdLocalAtUtc.value - : this.createdLocalAtUtc, - updatedLocalAtUtc: data.updatedLocalAtUtc.present - ? data.updatedLocalAtUtc.value - : this.updatedLocalAtUtc, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + lastInventoryAtUtc: data.lastInventoryAtUtc.present + ? data.lastInventoryAtUtc.value + : this.lastInventoryAtUtc, + lastSyncAtUtc: data.lastSyncAtUtc.present + ? data.lastSyncAtUtc.value + : this.lastSyncAtUtc, + parserVersion: data.parserVersion.present + ? data.parserVersion.value + : this.parserVersion, + projectionVersion: data.projectionVersion.present + ? data.projectionVersion.value + : this.projectionVersion, + createdAtUtc: data.createdAtUtc.present + ? data.createdAtUtc.value + : this.createdAtUtc, + updatedAtUtc: data.updatedAtUtc.present + ? data.updatedAtUtc.value + : this.updatedAtUtc, ); } @override String toString() { - return (StringBuffer('Task(') - ..write('accountId: $accountId, ') - ..write('taskListId: $taskListId, ') + return (StringBuffer('DavCollection(') ..write('id: $id, ') - ..write('kind: $kind, ') - ..write('etag: $etag, ') - ..write('title: $title, ') - ..write('updatedUtc: $updatedUtc, ') - ..write('selfLink: $selfLink, ') - ..write('parent: $parent, ') - ..write('position: $position, ') - ..write('notes: $notes, ') - ..write('status: $status, ') - ..write('dueUtc: $dueUtc, ') - ..write('completedUtc: $completedUtc, ') - ..write('providerStatus: $providerStatus, ') - ..write('bodyContent: $bodyContent, ') - ..write('bodyContentType: $bodyContentType, ') - ..write('microsoftDueDateTime: $microsoftDueDateTime, ') - ..write('microsoftDueTimeZone: $microsoftDueTimeZone, ') - ..write('microsoftStartDateTime: $microsoftStartDateTime, ') - ..write('microsoftStartTimeZone: $microsoftStartTimeZone, ') - ..write('microsoftReminderDateTime: $microsoftReminderDateTime, ') - ..write('microsoftReminderTimeZone: $microsoftReminderTimeZone, ') - ..write('microsoftIsReminderOn: $microsoftIsReminderOn, ') - ..write('microsoftCompletedDateTime: $microsoftCompletedDateTime, ') - ..write('microsoftCompletedTimeZone: $microsoftCompletedTimeZone, ') - ..write('recurrenceJson: $recurrenceJson, ') - ..write('importance: $importance, ') - ..write('categoriesJson: $categoriesJson, ') - ..write('hasAttachments: $hasAttachments, ') - ..write('providerMetadataJson: $providerMetadataJson, ') - ..write('deleted: $deleted, ') - ..write('hidden: $hidden, ') - ..write('linksJson: $linksJson, ') - ..write('webViewLink: $webViewLink, ') - ..write('assignmentInfoJson: $assignmentInfoJson, ') - ..write('rawJson: $rawJson, ') + ..write('accountId: $accountId, ') + ..write('hrefKey: $hrefKey, ') + ..write('requestUri: $requestUri, ') + ..write('displayName: $displayName, ') + ..write('description: $description, ') + ..write('resourceTypesJson: $resourceTypesJson, ') + ..write('supportedComponentMask: $supportedComponentMask, ') + ..write('supportedCalendarDataJson: $supportedCalendarDataJson, ') + ..write('supportedReportsJson: $supportedReportsJson, ') + ..write('currentUserPrivilegesJson: $currentUserPrivilegesJson, ') + ..write('ownerHref: $ownerHref, ') + ..write('safeDisplayMetadataJson: $safeDisplayMetadataJson, ') + ..write('color: $color, ') + ..write('sortOrder: $sortOrder, ') + ..write('calendarTimeZone: $calendarTimeZone, ') + ..write('calendarTimeZoneId: $calendarTimeZoneId, ') + ..write('scheduleTransparency: $scheduleTransparency, ') + ..write('maximumResourceSize: $maximumResourceSize, ') + ..write('maximumInstances: $maximumInstances, ') + ..write('syncToken: $syncToken, ') + ..write('ctag: $ctag, ') + ..write('readOnly: $readOnly, ') + ..write('eventProjectionEnabled: $eventProjectionEnabled, ') + ..write('taskProjectionEnabled: $taskProjectionEnabled, ') + ..write('eventsSelected: $eventsSelected, ') + ..write('tasksSelected: $tasksSelected, ') ..write('serverMissing: $serverMissing, ') - ..write('localDirty: $localDirty, ') - ..write('pendingDelete: $pendingDelete, ') - ..write('pendingMove: $pendingMove, ') - ..write('localCreated: $localCreated, ') - ..write('syncBaseUpdatedUtc: $syncBaseUpdatedUtc, ') - ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') - ..write('createdLocalAtUtc: $createdLocalAtUtc, ') - ..write('updatedLocalAtUtc: $updatedLocalAtUtc') + ..write('deleted: $deleted, ') + ..write('lastInventoryAtUtc: $lastInventoryAtUtc, ') + ..write('lastSyncAtUtc: $lastSyncAtUtc, ') + ..write('parserVersion: $parserVersion, ') + ..write('projectionVersion: $projectionVersion, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc') ..write(')')) .toString(); } @override int get hashCode => Object.hashAll([ - accountId, - taskListId, id, - kind, - etag, - title, - updatedUtc, - selfLink, - parent, - position, - notes, - status, - dueUtc, - completedUtc, - providerStatus, - bodyContent, - bodyContentType, - microsoftDueDateTime, - microsoftDueTimeZone, - microsoftStartDateTime, - microsoftStartTimeZone, - microsoftReminderDateTime, - microsoftReminderTimeZone, - microsoftIsReminderOn, - microsoftCompletedDateTime, - microsoftCompletedTimeZone, - recurrenceJson, - importance, - categoriesJson, - hasAttachments, - providerMetadataJson, - deleted, - hidden, - linksJson, - webViewLink, - assignmentInfoJson, - rawJson, + accountId, + hrefKey, + requestUri, + displayName, + description, + resourceTypesJson, + supportedComponentMask, + supportedCalendarDataJson, + supportedReportsJson, + currentUserPrivilegesJson, + ownerHref, + safeDisplayMetadataJson, + color, + sortOrder, + calendarTimeZone, + calendarTimeZoneId, + scheduleTransparency, + maximumResourceSize, + maximumInstances, + syncToken, + ctag, + readOnly, + eventProjectionEnabled, + taskProjectionEnabled, + eventsSelected, + tasksSelected, serverMissing, - localDirty, - pendingDelete, - pendingMove, - localCreated, - syncBaseUpdatedUtc, - lastSyncedAtUtc, - createdLocalAtUtc, - updatedLocalAtUtc, + deleted, + lastInventoryAtUtc, + lastSyncAtUtc, + parserVersion, + projectionVersion, + createdAtUtc, + updatedAtUtc, ]); @override bool operator ==(Object other) => identical(this, other) || - (other is Task && - other.accountId == this.accountId && - other.taskListId == this.taskListId && + (other is DavCollection && other.id == this.id && - other.kind == this.kind && - other.etag == this.etag && - other.title == this.title && - other.updatedUtc == this.updatedUtc && - other.selfLink == this.selfLink && - other.parent == this.parent && - other.position == this.position && - other.notes == this.notes && - other.status == this.status && - other.dueUtc == this.dueUtc && - other.completedUtc == this.completedUtc && - other.providerStatus == this.providerStatus && - other.bodyContent == this.bodyContent && - other.bodyContentType == this.bodyContentType && - other.microsoftDueDateTime == this.microsoftDueDateTime && - other.microsoftDueTimeZone == this.microsoftDueTimeZone && - other.microsoftStartDateTime == this.microsoftStartDateTime && - other.microsoftStartTimeZone == this.microsoftStartTimeZone && - other.microsoftReminderDateTime == this.microsoftReminderDateTime && - other.microsoftReminderTimeZone == this.microsoftReminderTimeZone && - other.microsoftIsReminderOn == this.microsoftIsReminderOn && - other.microsoftCompletedDateTime == this.microsoftCompletedDateTime && - other.microsoftCompletedTimeZone == this.microsoftCompletedTimeZone && - other.recurrenceJson == this.recurrenceJson && - other.importance == this.importance && - other.categoriesJson == this.categoriesJson && - other.hasAttachments == this.hasAttachments && - other.providerMetadataJson == this.providerMetadataJson && - other.deleted == this.deleted && - other.hidden == this.hidden && - other.linksJson == this.linksJson && - other.webViewLink == this.webViewLink && - other.assignmentInfoJson == this.assignmentInfoJson && - other.rawJson == this.rawJson && + other.accountId == this.accountId && + other.hrefKey == this.hrefKey && + other.requestUri == this.requestUri && + other.displayName == this.displayName && + other.description == this.description && + other.resourceTypesJson == this.resourceTypesJson && + other.supportedComponentMask == this.supportedComponentMask && + other.supportedCalendarDataJson == this.supportedCalendarDataJson && + other.supportedReportsJson == this.supportedReportsJson && + other.currentUserPrivilegesJson == this.currentUserPrivilegesJson && + other.ownerHref == this.ownerHref && + other.safeDisplayMetadataJson == this.safeDisplayMetadataJson && + other.color == this.color && + other.sortOrder == this.sortOrder && + other.calendarTimeZone == this.calendarTimeZone && + other.calendarTimeZoneId == this.calendarTimeZoneId && + other.scheduleTransparency == this.scheduleTransparency && + other.maximumResourceSize == this.maximumResourceSize && + other.maximumInstances == this.maximumInstances && + other.syncToken == this.syncToken && + other.ctag == this.ctag && + other.readOnly == this.readOnly && + other.eventProjectionEnabled == this.eventProjectionEnabled && + other.taskProjectionEnabled == this.taskProjectionEnabled && + other.eventsSelected == this.eventsSelected && + other.tasksSelected == this.tasksSelected && other.serverMissing == this.serverMissing && - other.localDirty == this.localDirty && - other.pendingDelete == this.pendingDelete && - other.pendingMove == this.pendingMove && - other.localCreated == this.localCreated && - other.syncBaseUpdatedUtc == this.syncBaseUpdatedUtc && - other.lastSyncedAtUtc == this.lastSyncedAtUtc && - other.createdLocalAtUtc == this.createdLocalAtUtc && - other.updatedLocalAtUtc == this.updatedLocalAtUtc); + other.deleted == this.deleted && + other.lastInventoryAtUtc == this.lastInventoryAtUtc && + other.lastSyncAtUtc == this.lastSyncAtUtc && + other.parserVersion == this.parserVersion && + other.projectionVersion == this.projectionVersion && + other.createdAtUtc == this.createdAtUtc && + other.updatedAtUtc == this.updatedAtUtc); } -class TasksCompanion extends UpdateCompanion { - final Value accountId; - final Value taskListId; +class DavCollectionsCompanion extends UpdateCompanion { final Value id; - final Value kind; - final Value etag; - final Value title; - final Value updatedUtc; - final Value selfLink; - final Value parent; - final Value position; - final Value notes; - final Value status; - final Value dueUtc; - final Value completedUtc; - final Value providerStatus; - final Value bodyContent; - final Value bodyContentType; - final Value microsoftDueDateTime; - final Value microsoftDueTimeZone; - final Value microsoftStartDateTime; - final Value microsoftStartTimeZone; - final Value microsoftReminderDateTime; - final Value microsoftReminderTimeZone; - final Value microsoftIsReminderOn; - final Value microsoftCompletedDateTime; - final Value microsoftCompletedTimeZone; - final Value recurrenceJson; - final Value importance; - final Value categoriesJson; - final Value hasAttachments; - final Value providerMetadataJson; - final Value deleted; - final Value hidden; - final Value linksJson; - final Value webViewLink; - final Value assignmentInfoJson; - final Value rawJson; + final Value accountId; + final Value hrefKey; + final Value requestUri; + final Value displayName; + final Value description; + final Value resourceTypesJson; + final Value supportedComponentMask; + final Value supportedCalendarDataJson; + final Value supportedReportsJson; + final Value currentUserPrivilegesJson; + final Value ownerHref; + final Value safeDisplayMetadataJson; + final Value color; + final Value sortOrder; + final Value calendarTimeZone; + final Value calendarTimeZoneId; + final Value scheduleTransparency; + final Value maximumResourceSize; + final Value maximumInstances; + final Value syncToken; + final Value ctag; + final Value readOnly; + final Value eventProjectionEnabled; + final Value taskProjectionEnabled; + final Value eventsSelected; + final Value tasksSelected; final Value serverMissing; - final Value localDirty; - final Value pendingDelete; - final Value pendingMove; - final Value localCreated; - final Value syncBaseUpdatedUtc; - final Value lastSyncedAtUtc; - final Value createdLocalAtUtc; - final Value updatedLocalAtUtc; + final Value deleted; + final Value lastInventoryAtUtc; + final Value lastSyncAtUtc; + final Value parserVersion; + final Value projectionVersion; + final Value createdAtUtc; + final Value updatedAtUtc; final Value rowid; - const TasksCompanion({ - this.accountId = const Value.absent(), - this.taskListId = const Value.absent(), + const DavCollectionsCompanion({ this.id = const Value.absent(), - this.kind = const Value.absent(), - this.etag = const Value.absent(), - this.title = const Value.absent(), - this.updatedUtc = const Value.absent(), - this.selfLink = const Value.absent(), - this.parent = const Value.absent(), - this.position = const Value.absent(), - this.notes = const Value.absent(), - this.status = const Value.absent(), - this.dueUtc = const Value.absent(), - this.completedUtc = const Value.absent(), - this.providerStatus = const Value.absent(), - this.bodyContent = const Value.absent(), - this.bodyContentType = const Value.absent(), - this.microsoftDueDateTime = const Value.absent(), - this.microsoftDueTimeZone = const Value.absent(), - this.microsoftStartDateTime = const Value.absent(), - this.microsoftStartTimeZone = const Value.absent(), - this.microsoftReminderDateTime = const Value.absent(), - this.microsoftReminderTimeZone = const Value.absent(), - this.microsoftIsReminderOn = const Value.absent(), - this.microsoftCompletedDateTime = const Value.absent(), - this.microsoftCompletedTimeZone = const Value.absent(), - this.recurrenceJson = const Value.absent(), - this.importance = const Value.absent(), - this.categoriesJson = const Value.absent(), - this.hasAttachments = const Value.absent(), - this.providerMetadataJson = const Value.absent(), - this.deleted = const Value.absent(), - this.hidden = const Value.absent(), - this.linksJson = const Value.absent(), - this.webViewLink = const Value.absent(), - this.assignmentInfoJson = const Value.absent(), - this.rawJson = const Value.absent(), + this.accountId = const Value.absent(), + this.hrefKey = const Value.absent(), + this.requestUri = const Value.absent(), + this.displayName = const Value.absent(), + this.description = const Value.absent(), + this.resourceTypesJson = const Value.absent(), + this.supportedComponentMask = const Value.absent(), + this.supportedCalendarDataJson = const Value.absent(), + this.supportedReportsJson = const Value.absent(), + this.currentUserPrivilegesJson = const Value.absent(), + this.ownerHref = const Value.absent(), + this.safeDisplayMetadataJson = const Value.absent(), + this.color = const Value.absent(), + this.sortOrder = const Value.absent(), + this.calendarTimeZone = const Value.absent(), + this.calendarTimeZoneId = const Value.absent(), + this.scheduleTransparency = const Value.absent(), + this.maximumResourceSize = const Value.absent(), + this.maximumInstances = const Value.absent(), + this.syncToken = const Value.absent(), + this.ctag = const Value.absent(), + this.readOnly = const Value.absent(), + this.eventProjectionEnabled = const Value.absent(), + this.taskProjectionEnabled = const Value.absent(), + this.eventsSelected = const Value.absent(), + this.tasksSelected = const Value.absent(), this.serverMissing = const Value.absent(), - this.localDirty = const Value.absent(), - this.pendingDelete = const Value.absent(), - this.pendingMove = const Value.absent(), - this.localCreated = const Value.absent(), - this.syncBaseUpdatedUtc = const Value.absent(), - this.lastSyncedAtUtc = const Value.absent(), - this.createdLocalAtUtc = const Value.absent(), - this.updatedLocalAtUtc = const Value.absent(), + this.deleted = const Value.absent(), + this.lastInventoryAtUtc = const Value.absent(), + this.lastSyncAtUtc = const Value.absent(), + this.parserVersion = const Value.absent(), + this.projectionVersion = const Value.absent(), + this.createdAtUtc = const Value.absent(), + this.updatedAtUtc = const Value.absent(), this.rowid = const Value.absent(), }); - TasksCompanion.insert({ - required String accountId, - required String taskListId, + DavCollectionsCompanion.insert({ required String id, - this.kind = const Value.absent(), - this.etag = const Value.absent(), - required String title, - this.updatedUtc = const Value.absent(), - this.selfLink = const Value.absent(), - this.parent = const Value.absent(), - this.position = const Value.absent(), - this.notes = const Value.absent(), - this.status = const Value.absent(), - this.dueUtc = const Value.absent(), - this.completedUtc = const Value.absent(), - this.providerStatus = const Value.absent(), - this.bodyContent = const Value.absent(), - this.bodyContentType = const Value.absent(), - this.microsoftDueDateTime = const Value.absent(), - this.microsoftDueTimeZone = const Value.absent(), - this.microsoftStartDateTime = const Value.absent(), - this.microsoftStartTimeZone = const Value.absent(), - this.microsoftReminderDateTime = const Value.absent(), - this.microsoftReminderTimeZone = const Value.absent(), - this.microsoftIsReminderOn = const Value.absent(), - this.microsoftCompletedDateTime = const Value.absent(), - this.microsoftCompletedTimeZone = const Value.absent(), - this.recurrenceJson = const Value.absent(), - this.importance = const Value.absent(), - this.categoriesJson = const Value.absent(), - this.hasAttachments = const Value.absent(), - this.providerMetadataJson = const Value.absent(), - this.deleted = const Value.absent(), - this.hidden = const Value.absent(), - this.linksJson = const Value.absent(), - this.webViewLink = const Value.absent(), - this.assignmentInfoJson = const Value.absent(), - required String rawJson, + required String accountId, + required String hrefKey, + required String requestUri, + required String displayName, + this.description = const Value.absent(), + this.resourceTypesJson = const Value.absent(), + this.supportedComponentMask = const Value.absent(), + this.supportedCalendarDataJson = const Value.absent(), + this.supportedReportsJson = const Value.absent(), + this.currentUserPrivilegesJson = const Value.absent(), + this.ownerHref = const Value.absent(), + this.safeDisplayMetadataJson = const Value.absent(), + this.color = const Value.absent(), + this.sortOrder = const Value.absent(), + this.calendarTimeZone = const Value.absent(), + this.calendarTimeZoneId = const Value.absent(), + this.scheduleTransparency = const Value.absent(), + this.maximumResourceSize = const Value.absent(), + this.maximumInstances = const Value.absent(), + this.syncToken = const Value.absent(), + this.ctag = const Value.absent(), + this.readOnly = const Value.absent(), + this.eventProjectionEnabled = const Value.absent(), + this.taskProjectionEnabled = const Value.absent(), + this.eventsSelected = const Value.absent(), + this.tasksSelected = const Value.absent(), this.serverMissing = const Value.absent(), - this.localDirty = const Value.absent(), - this.pendingDelete = const Value.absent(), - this.pendingMove = const Value.absent(), - this.localCreated = const Value.absent(), - this.syncBaseUpdatedUtc = const Value.absent(), - this.lastSyncedAtUtc = const Value.absent(), - required String createdLocalAtUtc, - required String updatedLocalAtUtc, + this.deleted = const Value.absent(), + this.lastInventoryAtUtc = const Value.absent(), + this.lastSyncAtUtc = const Value.absent(), + this.parserVersion = const Value.absent(), + this.projectionVersion = const Value.absent(), + required String createdAtUtc, + required String updatedAtUtc, this.rowid = const Value.absent(), - }) : accountId = Value(accountId), - taskListId = Value(taskListId), - id = Value(id), - title = Value(title), - rawJson = Value(rawJson), - createdLocalAtUtc = Value(createdLocalAtUtc), - updatedLocalAtUtc = Value(updatedLocalAtUtc); - static Insertable custom({ - Expression? accountId, - Expression? taskListId, + }) : id = Value(id), + accountId = Value(accountId), + hrefKey = Value(hrefKey), + requestUri = Value(requestUri), + displayName = Value(displayName), + createdAtUtc = Value(createdAtUtc), + updatedAtUtc = Value(updatedAtUtc); + static Insertable custom({ Expression? id, - Expression? kind, - Expression? etag, - Expression? title, - Expression? updatedUtc, - Expression? selfLink, - Expression? parent, - Expression? position, - Expression? notes, - Expression? status, - Expression? dueUtc, - Expression? completedUtc, - Expression? providerStatus, - Expression? bodyContent, - Expression? bodyContentType, - Expression? microsoftDueDateTime, - Expression? microsoftDueTimeZone, - Expression? microsoftStartDateTime, - Expression? microsoftStartTimeZone, - Expression? microsoftReminderDateTime, - Expression? microsoftReminderTimeZone, - Expression? microsoftIsReminderOn, - Expression? microsoftCompletedDateTime, - Expression? microsoftCompletedTimeZone, - Expression? recurrenceJson, - Expression? importance, - Expression? categoriesJson, - Expression? hasAttachments, - Expression? providerMetadataJson, - Expression? deleted, - Expression? hidden, - Expression? linksJson, - Expression? webViewLink, - Expression? assignmentInfoJson, - Expression? rawJson, + Expression? accountId, + Expression? hrefKey, + Expression? requestUri, + Expression? displayName, + Expression? description, + Expression? resourceTypesJson, + Expression? supportedComponentMask, + Expression? supportedCalendarDataJson, + Expression? supportedReportsJson, + Expression? currentUserPrivilegesJson, + Expression? ownerHref, + Expression? safeDisplayMetadataJson, + Expression? color, + Expression? sortOrder, + Expression? calendarTimeZone, + Expression? calendarTimeZoneId, + Expression? scheduleTransparency, + Expression? maximumResourceSize, + Expression? maximumInstances, + Expression? syncToken, + Expression? ctag, + Expression? readOnly, + Expression? eventProjectionEnabled, + Expression? taskProjectionEnabled, + Expression? eventsSelected, + Expression? tasksSelected, Expression? serverMissing, - Expression? localDirty, - Expression? pendingDelete, - Expression? pendingMove, - Expression? localCreated, - Expression? syncBaseUpdatedUtc, - Expression? lastSyncedAtUtc, - Expression? createdLocalAtUtc, - Expression? updatedLocalAtUtc, + Expression? deleted, + Expression? lastInventoryAtUtc, + Expression? lastSyncAtUtc, + Expression? parserVersion, + Expression? projectionVersion, + Expression? createdAtUtc, + Expression? updatedAtUtc, Expression? rowid, }) { return RawValuesInsertable({ - if (accountId != null) 'account_id': accountId, - if (taskListId != null) 'task_list_id': taskListId, if (id != null) 'id': id, - if (kind != null) 'kind': kind, - if (etag != null) 'etag': etag, - if (title != null) 'title': title, - if (updatedUtc != null) 'updated_utc': updatedUtc, - if (selfLink != null) 'self_link': selfLink, - if (parent != null) 'parent': parent, - if (position != null) 'position': position, - if (notes != null) 'notes': notes, - if (status != null) 'status': status, - if (dueUtc != null) 'due_utc': dueUtc, - if (completedUtc != null) 'completed_utc': completedUtc, - if (providerStatus != null) 'provider_status': providerStatus, - if (bodyContent != null) 'body_content': bodyContent, - if (bodyContentType != null) 'body_content_type': bodyContentType, - if (microsoftDueDateTime != null) - 'microsoft_due_date_time': microsoftDueDateTime, - if (microsoftDueTimeZone != null) - 'microsoft_due_time_zone': microsoftDueTimeZone, - if (microsoftStartDateTime != null) - 'microsoft_start_date_time': microsoftStartDateTime, - if (microsoftStartTimeZone != null) - 'microsoft_start_time_zone': microsoftStartTimeZone, - if (microsoftReminderDateTime != null) - 'microsoft_reminder_date_time': microsoftReminderDateTime, - if (microsoftReminderTimeZone != null) - 'microsoft_reminder_time_zone': microsoftReminderTimeZone, - if (microsoftIsReminderOn != null) - 'microsoft_is_reminder_on': microsoftIsReminderOn, - if (microsoftCompletedDateTime != null) - 'microsoft_completed_date_time': microsoftCompletedDateTime, - if (microsoftCompletedTimeZone != null) - 'microsoft_completed_time_zone': microsoftCompletedTimeZone, - if (recurrenceJson != null) 'recurrence_json': recurrenceJson, - if (importance != null) 'importance': importance, - if (categoriesJson != null) 'categories_json': categoriesJson, - if (hasAttachments != null) 'has_attachments': hasAttachments, - if (providerMetadataJson != null) - 'provider_metadata_json': providerMetadataJson, - if (deleted != null) 'deleted': deleted, - if (hidden != null) 'hidden': hidden, - if (linksJson != null) 'links_json': linksJson, - if (webViewLink != null) 'web_view_link': webViewLink, - if (assignmentInfoJson != null) - 'assignment_info_json': assignmentInfoJson, - if (rawJson != null) 'raw_json': rawJson, + if (accountId != null) 'account_id': accountId, + if (hrefKey != null) 'href_key': hrefKey, + if (requestUri != null) 'request_uri': requestUri, + if (displayName != null) 'display_name': displayName, + if (description != null) 'description': description, + if (resourceTypesJson != null) 'resource_types_json': resourceTypesJson, + if (supportedComponentMask != null) + 'supported_component_mask': supportedComponentMask, + if (supportedCalendarDataJson != null) + 'supported_calendar_data_json': supportedCalendarDataJson, + if (supportedReportsJson != null) + 'supported_reports_json': supportedReportsJson, + if (currentUserPrivilegesJson != null) + 'current_user_privileges_json': currentUserPrivilegesJson, + if (ownerHref != null) 'owner_href': ownerHref, + if (safeDisplayMetadataJson != null) + 'safe_display_metadata_json': safeDisplayMetadataJson, + if (color != null) 'color': color, + if (sortOrder != null) 'sort_order': sortOrder, + if (calendarTimeZone != null) 'calendar_time_zone': calendarTimeZone, + if (calendarTimeZoneId != null) + 'calendar_time_zone_id': calendarTimeZoneId, + if (scheduleTransparency != null) + 'schedule_transparency': scheduleTransparency, + if (maximumResourceSize != null) + 'maximum_resource_size': maximumResourceSize, + if (maximumInstances != null) 'maximum_instances': maximumInstances, + if (syncToken != null) 'sync_token': syncToken, + if (ctag != null) 'ctag': ctag, + if (readOnly != null) 'read_only': readOnly, + if (eventProjectionEnabled != null) + 'event_projection_enabled': eventProjectionEnabled, + if (taskProjectionEnabled != null) + 'task_projection_enabled': taskProjectionEnabled, + if (eventsSelected != null) 'events_selected': eventsSelected, + if (tasksSelected != null) 'tasks_selected': tasksSelected, if (serverMissing != null) 'server_missing': serverMissing, - if (localDirty != null) 'local_dirty': localDirty, - if (pendingDelete != null) 'pending_delete': pendingDelete, - if (pendingMove != null) 'pending_move': pendingMove, - if (localCreated != null) 'local_created': localCreated, - if (syncBaseUpdatedUtc != null) - 'sync_base_updated_utc': syncBaseUpdatedUtc, - if (lastSyncedAtUtc != null) 'last_synced_at_utc': lastSyncedAtUtc, - if (createdLocalAtUtc != null) 'created_local_at_utc': createdLocalAtUtc, - if (updatedLocalAtUtc != null) 'updated_local_at_utc': updatedLocalAtUtc, + if (deleted != null) 'deleted': deleted, + if (lastInventoryAtUtc != null) + 'last_inventory_at_utc': lastInventoryAtUtc, + if (lastSyncAtUtc != null) 'last_sync_at_utc': lastSyncAtUtc, + if (parserVersion != null) 'parser_version': parserVersion, + if (projectionVersion != null) 'projection_version': projectionVersion, + if (createdAtUtc != null) 'created_at_utc': createdAtUtc, + if (updatedAtUtc != null) 'updated_at_utc': updatedAtUtc, if (rowid != null) 'rowid': rowid, }); } - TasksCompanion copyWith({ - Value? accountId, - Value? taskListId, + DavCollectionsCompanion copyWith({ Value? id, - Value? kind, - Value? etag, - Value? title, - Value? updatedUtc, - Value? selfLink, - Value? parent, - Value? position, - Value? notes, - Value? status, - Value? dueUtc, - Value? completedUtc, - Value? providerStatus, - Value? bodyContent, - Value? bodyContentType, - Value? microsoftDueDateTime, - Value? microsoftDueTimeZone, - Value? microsoftStartDateTime, - Value? microsoftStartTimeZone, - Value? microsoftReminderDateTime, - Value? microsoftReminderTimeZone, - Value? microsoftIsReminderOn, - Value? microsoftCompletedDateTime, - Value? microsoftCompletedTimeZone, - Value? recurrenceJson, - Value? importance, - Value? categoriesJson, - Value? hasAttachments, - Value? providerMetadataJson, - Value? deleted, - Value? hidden, - Value? linksJson, - Value? webViewLink, - Value? assignmentInfoJson, - Value? rawJson, + Value? accountId, + Value? hrefKey, + Value? requestUri, + Value? displayName, + Value? description, + Value? resourceTypesJson, + Value? supportedComponentMask, + Value? supportedCalendarDataJson, + Value? supportedReportsJson, + Value? currentUserPrivilegesJson, + Value? ownerHref, + Value? safeDisplayMetadataJson, + Value? color, + Value? sortOrder, + Value? calendarTimeZone, + Value? calendarTimeZoneId, + Value? scheduleTransparency, + Value? maximumResourceSize, + Value? maximumInstances, + Value? syncToken, + Value? ctag, + Value? readOnly, + Value? eventProjectionEnabled, + Value? taskProjectionEnabled, + Value? eventsSelected, + Value? tasksSelected, Value? serverMissing, - Value? localDirty, - Value? pendingDelete, - Value? pendingMove, - Value? localCreated, - Value? syncBaseUpdatedUtc, - Value? lastSyncedAtUtc, - Value? createdLocalAtUtc, - Value? updatedLocalAtUtc, + Value? deleted, + Value? lastInventoryAtUtc, + Value? lastSyncAtUtc, + Value? parserVersion, + Value? projectionVersion, + Value? createdAtUtc, + Value? updatedAtUtc, Value? rowid, }) { - return TasksCompanion( - accountId: accountId ?? this.accountId, - taskListId: taskListId ?? this.taskListId, + return DavCollectionsCompanion( id: id ?? this.id, - kind: kind ?? this.kind, - etag: etag ?? this.etag, - title: title ?? this.title, - updatedUtc: updatedUtc ?? this.updatedUtc, - selfLink: selfLink ?? this.selfLink, - parent: parent ?? this.parent, - position: position ?? this.position, - notes: notes ?? this.notes, - status: status ?? this.status, - dueUtc: dueUtc ?? this.dueUtc, - completedUtc: completedUtc ?? this.completedUtc, - providerStatus: providerStatus ?? this.providerStatus, - bodyContent: bodyContent ?? this.bodyContent, - bodyContentType: bodyContentType ?? this.bodyContentType, - microsoftDueDateTime: microsoftDueDateTime ?? this.microsoftDueDateTime, - microsoftDueTimeZone: microsoftDueTimeZone ?? this.microsoftDueTimeZone, - microsoftStartDateTime: - microsoftStartDateTime ?? this.microsoftStartDateTime, - microsoftStartTimeZone: - microsoftStartTimeZone ?? this.microsoftStartTimeZone, - microsoftReminderDateTime: - microsoftReminderDateTime ?? this.microsoftReminderDateTime, - microsoftReminderTimeZone: - microsoftReminderTimeZone ?? this.microsoftReminderTimeZone, - microsoftIsReminderOn: - microsoftIsReminderOn ?? this.microsoftIsReminderOn, - microsoftCompletedDateTime: - microsoftCompletedDateTime ?? this.microsoftCompletedDateTime, - microsoftCompletedTimeZone: - microsoftCompletedTimeZone ?? this.microsoftCompletedTimeZone, - recurrenceJson: recurrenceJson ?? this.recurrenceJson, - importance: importance ?? this.importance, - categoriesJson: categoriesJson ?? this.categoriesJson, - hasAttachments: hasAttachments ?? this.hasAttachments, - providerMetadataJson: providerMetadataJson ?? this.providerMetadataJson, - deleted: deleted ?? this.deleted, - hidden: hidden ?? this.hidden, - linksJson: linksJson ?? this.linksJson, - webViewLink: webViewLink ?? this.webViewLink, - assignmentInfoJson: assignmentInfoJson ?? this.assignmentInfoJson, - rawJson: rawJson ?? this.rawJson, + accountId: accountId ?? this.accountId, + hrefKey: hrefKey ?? this.hrefKey, + requestUri: requestUri ?? this.requestUri, + displayName: displayName ?? this.displayName, + description: description ?? this.description, + resourceTypesJson: resourceTypesJson ?? this.resourceTypesJson, + supportedComponentMask: + supportedComponentMask ?? this.supportedComponentMask, + supportedCalendarDataJson: + supportedCalendarDataJson ?? this.supportedCalendarDataJson, + supportedReportsJson: supportedReportsJson ?? this.supportedReportsJson, + currentUserPrivilegesJson: + currentUserPrivilegesJson ?? this.currentUserPrivilegesJson, + ownerHref: ownerHref ?? this.ownerHref, + safeDisplayMetadataJson: + safeDisplayMetadataJson ?? this.safeDisplayMetadataJson, + color: color ?? this.color, + sortOrder: sortOrder ?? this.sortOrder, + calendarTimeZone: calendarTimeZone ?? this.calendarTimeZone, + calendarTimeZoneId: calendarTimeZoneId ?? this.calendarTimeZoneId, + scheduleTransparency: scheduleTransparency ?? this.scheduleTransparency, + maximumResourceSize: maximumResourceSize ?? this.maximumResourceSize, + maximumInstances: maximumInstances ?? this.maximumInstances, + syncToken: syncToken ?? this.syncToken, + ctag: ctag ?? this.ctag, + readOnly: readOnly ?? this.readOnly, + eventProjectionEnabled: + eventProjectionEnabled ?? this.eventProjectionEnabled, + taskProjectionEnabled: + taskProjectionEnabled ?? this.taskProjectionEnabled, + eventsSelected: eventsSelected ?? this.eventsSelected, + tasksSelected: tasksSelected ?? this.tasksSelected, serverMissing: serverMissing ?? this.serverMissing, - localDirty: localDirty ?? this.localDirty, - pendingDelete: pendingDelete ?? this.pendingDelete, - pendingMove: pendingMove ?? this.pendingMove, - localCreated: localCreated ?? this.localCreated, - syncBaseUpdatedUtc: syncBaseUpdatedUtc ?? this.syncBaseUpdatedUtc, - lastSyncedAtUtc: lastSyncedAtUtc ?? this.lastSyncedAtUtc, - createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, - updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, + deleted: deleted ?? this.deleted, + lastInventoryAtUtc: lastInventoryAtUtc ?? this.lastInventoryAtUtc, + lastSyncAtUtc: lastSyncAtUtc ?? this.lastSyncAtUtc, + parserVersion: parserVersion ?? this.parserVersion, + projectionVersion: projectionVersion ?? this.projectionVersion, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, rowid: rowid ?? this.rowid, ); } @@ -4515,163 +3926,126 @@ class TasksCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (taskListId.present) { - map['task_list_id'] = Variable(taskListId.value); - } if (id.present) { map['id'] = Variable(id.value); } - if (kind.present) { - map['kind'] = Variable(kind.value); - } - if (etag.present) { - map['etag'] = Variable(etag.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (updatedUtc.present) { - map['updated_utc'] = Variable(updatedUtc.value); - } - if (selfLink.present) { - map['self_link'] = Variable(selfLink.value); - } - if (parent.present) { - map['parent'] = Variable(parent.value); - } - if (position.present) { - map['position'] = Variable(position.value); - } - if (notes.present) { - map['notes'] = Variable(notes.value); - } - if (status.present) { - map['status'] = Variable(status.value); + if (accountId.present) { + map['account_id'] = Variable(accountId.value); } - if (dueUtc.present) { - map['due_utc'] = Variable(dueUtc.value); + if (hrefKey.present) { + map['href_key'] = Variable(hrefKey.value); } - if (completedUtc.present) { - map['completed_utc'] = Variable(completedUtc.value); + if (requestUri.present) { + map['request_uri'] = Variable(requestUri.value); } - if (providerStatus.present) { - map['provider_status'] = Variable(providerStatus.value); + if (displayName.present) { + map['display_name'] = Variable(displayName.value); } - if (bodyContent.present) { - map['body_content'] = Variable(bodyContent.value); + if (description.present) { + map['description'] = Variable(description.value); } - if (bodyContentType.present) { - map['body_content_type'] = Variable(bodyContentType.value); + if (resourceTypesJson.present) { + map['resource_types_json'] = Variable(resourceTypesJson.value); } - if (microsoftDueDateTime.present) { - map['microsoft_due_date_time'] = Variable( - microsoftDueDateTime.value, + if (supportedComponentMask.present) { + map['supported_component_mask'] = Variable( + supportedComponentMask.value, ); } - if (microsoftDueTimeZone.present) { - map['microsoft_due_time_zone'] = Variable( - microsoftDueTimeZone.value, + if (supportedCalendarDataJson.present) { + map['supported_calendar_data_json'] = Variable( + supportedCalendarDataJson.value, ); } - if (microsoftStartDateTime.present) { - map['microsoft_start_date_time'] = Variable( - microsoftStartDateTime.value, + if (supportedReportsJson.present) { + map['supported_reports_json'] = Variable( + supportedReportsJson.value, ); } - if (microsoftStartTimeZone.present) { - map['microsoft_start_time_zone'] = Variable( - microsoftStartTimeZone.value, + if (currentUserPrivilegesJson.present) { + map['current_user_privileges_json'] = Variable( + currentUserPrivilegesJson.value, ); } - if (microsoftReminderDateTime.present) { - map['microsoft_reminder_date_time'] = Variable( - microsoftReminderDateTime.value, - ); + if (ownerHref.present) { + map['owner_href'] = Variable(ownerHref.value); } - if (microsoftReminderTimeZone.present) { - map['microsoft_reminder_time_zone'] = Variable( - microsoftReminderTimeZone.value, + if (safeDisplayMetadataJson.present) { + map['safe_display_metadata_json'] = Variable( + safeDisplayMetadataJson.value, ); } - if (microsoftIsReminderOn.present) { - map['microsoft_is_reminder_on'] = Variable( - microsoftIsReminderOn.value, - ); + if (color.present) { + map['color'] = Variable(color.value); } - if (microsoftCompletedDateTime.present) { - map['microsoft_completed_date_time'] = Variable( - microsoftCompletedDateTime.value, - ); + if (sortOrder.present) { + map['sort_order'] = Variable(sortOrder.value); } - if (microsoftCompletedTimeZone.present) { - map['microsoft_completed_time_zone'] = Variable( - microsoftCompletedTimeZone.value, - ); + if (calendarTimeZone.present) { + map['calendar_time_zone'] = Variable(calendarTimeZone.value); } - if (recurrenceJson.present) { - map['recurrence_json'] = Variable(recurrenceJson.value); + if (calendarTimeZoneId.present) { + map['calendar_time_zone_id'] = Variable(calendarTimeZoneId.value); } - if (importance.present) { - map['importance'] = Variable(importance.value); + if (scheduleTransparency.present) { + map['schedule_transparency'] = Variable( + scheduleTransparency.value, + ); } - if (categoriesJson.present) { - map['categories_json'] = Variable(categoriesJson.value); + if (maximumResourceSize.present) { + map['maximum_resource_size'] = Variable(maximumResourceSize.value); } - if (hasAttachments.present) { - map['has_attachments'] = Variable(hasAttachments.value); + if (maximumInstances.present) { + map['maximum_instances'] = Variable(maximumInstances.value); } - if (providerMetadataJson.present) { - map['provider_metadata_json'] = Variable( - providerMetadataJson.value, - ); + if (syncToken.present) { + map['sync_token'] = Variable(syncToken.value); } - if (deleted.present) { - map['deleted'] = Variable(deleted.value); + if (ctag.present) { + map['ctag'] = Variable(ctag.value); } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); + if (readOnly.present) { + map['read_only'] = Variable(readOnly.value); } - if (linksJson.present) { - map['links_json'] = Variable(linksJson.value); + if (eventProjectionEnabled.present) { + map['event_projection_enabled'] = Variable( + eventProjectionEnabled.value, + ); } - if (webViewLink.present) { - map['web_view_link'] = Variable(webViewLink.value); + if (taskProjectionEnabled.present) { + map['task_projection_enabled'] = Variable( + taskProjectionEnabled.value, + ); } - if (assignmentInfoJson.present) { - map['assignment_info_json'] = Variable(assignmentInfoJson.value); + if (eventsSelected.present) { + map['events_selected'] = Variable(eventsSelected.value); } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); + if (tasksSelected.present) { + map['tasks_selected'] = Variable(tasksSelected.value); } if (serverMissing.present) { map['server_missing'] = Variable(serverMissing.value); } - if (localDirty.present) { - map['local_dirty'] = Variable(localDirty.value); - } - if (pendingDelete.present) { - map['pending_delete'] = Variable(pendingDelete.value); + if (deleted.present) { + map['deleted'] = Variable(deleted.value); } - if (pendingMove.present) { - map['pending_move'] = Variable(pendingMove.value); + if (lastInventoryAtUtc.present) { + map['last_inventory_at_utc'] = Variable(lastInventoryAtUtc.value); } - if (localCreated.present) { - map['local_created'] = Variable(localCreated.value); + if (lastSyncAtUtc.present) { + map['last_sync_at_utc'] = Variable(lastSyncAtUtc.value); } - if (syncBaseUpdatedUtc.present) { - map['sync_base_updated_utc'] = Variable(syncBaseUpdatedUtc.value); + if (parserVersion.present) { + map['parser_version'] = Variable(parserVersion.value); } - if (lastSyncedAtUtc.present) { - map['last_synced_at_utc'] = Variable(lastSyncedAtUtc.value); + if (projectionVersion.present) { + map['projection_version'] = Variable(projectionVersion.value); } - if (createdLocalAtUtc.present) { - map['created_local_at_utc'] = Variable(createdLocalAtUtc.value); + if (createdAtUtc.present) { + map['created_at_utc'] = Variable(createdAtUtc.value); } - if (updatedLocalAtUtc.present) { - map['updated_local_at_utc'] = Variable(updatedLocalAtUtc.value); + if (updatedAtUtc.present) { + map['updated_at_utc'] = Variable(updatedAtUtc.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -4681,65 +4055,54 @@ class TasksCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('TasksCompanion(') - ..write('accountId: $accountId, ') - ..write('taskListId: $taskListId, ') + return (StringBuffer('DavCollectionsCompanion(') ..write('id: $id, ') - ..write('kind: $kind, ') - ..write('etag: $etag, ') - ..write('title: $title, ') - ..write('updatedUtc: $updatedUtc, ') - ..write('selfLink: $selfLink, ') - ..write('parent: $parent, ') - ..write('position: $position, ') - ..write('notes: $notes, ') - ..write('status: $status, ') - ..write('dueUtc: $dueUtc, ') - ..write('completedUtc: $completedUtc, ') - ..write('providerStatus: $providerStatus, ') - ..write('bodyContent: $bodyContent, ') - ..write('bodyContentType: $bodyContentType, ') - ..write('microsoftDueDateTime: $microsoftDueDateTime, ') - ..write('microsoftDueTimeZone: $microsoftDueTimeZone, ') - ..write('microsoftStartDateTime: $microsoftStartDateTime, ') - ..write('microsoftStartTimeZone: $microsoftStartTimeZone, ') - ..write('microsoftReminderDateTime: $microsoftReminderDateTime, ') - ..write('microsoftReminderTimeZone: $microsoftReminderTimeZone, ') - ..write('microsoftIsReminderOn: $microsoftIsReminderOn, ') - ..write('microsoftCompletedDateTime: $microsoftCompletedDateTime, ') - ..write('microsoftCompletedTimeZone: $microsoftCompletedTimeZone, ') - ..write('recurrenceJson: $recurrenceJson, ') - ..write('importance: $importance, ') - ..write('categoriesJson: $categoriesJson, ') - ..write('hasAttachments: $hasAttachments, ') - ..write('providerMetadataJson: $providerMetadataJson, ') - ..write('deleted: $deleted, ') - ..write('hidden: $hidden, ') - ..write('linksJson: $linksJson, ') - ..write('webViewLink: $webViewLink, ') - ..write('assignmentInfoJson: $assignmentInfoJson, ') - ..write('rawJson: $rawJson, ') + ..write('accountId: $accountId, ') + ..write('hrefKey: $hrefKey, ') + ..write('requestUri: $requestUri, ') + ..write('displayName: $displayName, ') + ..write('description: $description, ') + ..write('resourceTypesJson: $resourceTypesJson, ') + ..write('supportedComponentMask: $supportedComponentMask, ') + ..write('supportedCalendarDataJson: $supportedCalendarDataJson, ') + ..write('supportedReportsJson: $supportedReportsJson, ') + ..write('currentUserPrivilegesJson: $currentUserPrivilegesJson, ') + ..write('ownerHref: $ownerHref, ') + ..write('safeDisplayMetadataJson: $safeDisplayMetadataJson, ') + ..write('color: $color, ') + ..write('sortOrder: $sortOrder, ') + ..write('calendarTimeZone: $calendarTimeZone, ') + ..write('calendarTimeZoneId: $calendarTimeZoneId, ') + ..write('scheduleTransparency: $scheduleTransparency, ') + ..write('maximumResourceSize: $maximumResourceSize, ') + ..write('maximumInstances: $maximumInstances, ') + ..write('syncToken: $syncToken, ') + ..write('ctag: $ctag, ') + ..write('readOnly: $readOnly, ') + ..write('eventProjectionEnabled: $eventProjectionEnabled, ') + ..write('taskProjectionEnabled: $taskProjectionEnabled, ') + ..write('eventsSelected: $eventsSelected, ') + ..write('tasksSelected: $tasksSelected, ') ..write('serverMissing: $serverMissing, ') - ..write('localDirty: $localDirty, ') - ..write('pendingDelete: $pendingDelete, ') - ..write('pendingMove: $pendingMove, ') - ..write('localCreated: $localCreated, ') - ..write('syncBaseUpdatedUtc: $syncBaseUpdatedUtc, ') - ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') - ..write('createdLocalAtUtc: $createdLocalAtUtc, ') - ..write('updatedLocalAtUtc: $updatedLocalAtUtc, ') + ..write('deleted: $deleted, ') + ..write('lastInventoryAtUtc: $lastInventoryAtUtc, ') + ..write('lastSyncAtUtc: $lastSyncAtUtc, ') + ..write('parserVersion: $parserVersion, ') + ..write('projectionVersion: $projectionVersion, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $PendingOpsTable extends PendingOps - with TableInfo<$PendingOpsTable, PendingOp> { +class $DavObjectsTable extends DavObjects + with TableInfo<$DavObjectsTable, DavObject> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $PendingOpsTable(this.attachedDatabase, [this._alias]); + $DavObjectsTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -4763,281 +4126,255 @@ class $PendingOpsTable extends PendingOps 'REFERENCES accounts (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', - ); - @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _entityTypeMeta = const VerificationMeta( - 'entityType', + static const VerificationMeta _collectionIdMeta = const VerificationMeta( + 'collectionId', ); @override - late final GeneratedColumn entityType = GeneratedColumn( - 'entity_type', + late final GeneratedColumn collectionId = GeneratedColumn( + 'collection_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE CASCADE', + ), ); - static const VerificationMeta _operationMeta = const VerificationMeta( - 'operation', + static const VerificationMeta _hrefKeyMeta = const VerificationMeta( + 'hrefKey', ); @override - late final GeneratedColumn operation = GeneratedColumn( - 'operation', + late final GeneratedColumn hrefKey = GeneratedColumn( + 'href_key', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _operationTypeMeta = const VerificationMeta( - 'operationType', - ); - @override - late final GeneratedColumn operationType = GeneratedColumn( - 'operation_type', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _taskListIdMeta = const VerificationMeta( - 'taskListId', + static const VerificationMeta _requestUriMeta = const VerificationMeta( + 'requestUri', ); @override - late final GeneratedColumn taskListId = GeneratedColumn( - 'task_list_id', + late final GeneratedColumn requestUri = GeneratedColumn( + 'request_uri', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _taskIdMeta = const VerificationMeta('taskId'); + static const VerificationMeta _etagMeta = const VerificationMeta('etag'); @override - late final GeneratedColumn taskId = GeneratedColumn( - 'task_id', + late final GeneratedColumn etag = GeneratedColumn( + 'etag', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _calendarSourceIdMeta = const VerificationMeta( - 'calendarSourceId', + static const VerificationMeta _contentTypeMeta = const VerificationMeta( + 'contentType', ); @override - late final GeneratedColumn calendarSourceId = GeneratedColumn( - 'calendar_source_id', + late final GeneratedColumn contentType = GeneratedColumn( + 'content_type', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _providerCalendarIdMeta = - const VerificationMeta('providerCalendarId'); + static const VerificationMeta _dominantComponentTypeMeta = + const VerificationMeta('dominantComponentType'); @override - late final GeneratedColumn providerCalendarId = + late final GeneratedColumn dominantComponentType = GeneratedColumn( - 'provider_calendar_id', + 'dominant_component_type', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _eventIdMeta = const VerificationMeta( - 'eventId', + static const VerificationMeta _componentMaskMeta = const VerificationMeta( + 'componentMask', ); @override - late final GeneratedColumn eventId = GeneratedColumn( - 'event_id', + late final GeneratedColumn componentMask = GeneratedColumn( + 'component_mask', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.int, requiredDuringInsert: false, + defaultValue: const Constant(0), ); - static const VerificationMeta _localTempIdMeta = const VerificationMeta( - 'localTempId', + static const VerificationMeta _primaryUidMeta = const VerificationMeta( + 'primaryUid', ); @override - late final GeneratedColumn localTempId = GeneratedColumn( - 'local_temp_id', + late final GeneratedColumn primaryUid = GeneratedColumn( + 'primary_uid', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _dependsOnOpIdMeta = const VerificationMeta( - 'dependsOnOpId', + static const VerificationMeta _rawIcsBodyMeta = const VerificationMeta( + 'rawIcsBody', ); @override - late final GeneratedColumn dependsOnOpId = GeneratedColumn( - 'depends_on_op_id', + late final GeneratedColumn rawIcsBody = GeneratedColumn( + 'raw_ics_body', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _requestJsonMeta = const VerificationMeta( - 'requestJson', + static const VerificationMeta _rawBodyHashMeta = const VerificationMeta( + 'rawBodyHash', ); @override - late final GeneratedColumn requestJson = GeneratedColumn( - 'request_json', + late final GeneratedColumn rawBodyHash = GeneratedColumn( + 'raw_body_hash', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _baselineUpdatedUtcMeta = - const VerificationMeta('baselineUpdatedUtc'); - @override - late final GeneratedColumn baselineUpdatedUtc = - GeneratedColumn( - 'baseline_updated_utc', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _baselineRawJsonMeta = const VerificationMeta( - 'baselineRawJson', + static const VerificationMeta _semanticHashMeta = const VerificationMeta( + 'semanticHash', ); @override - late final GeneratedColumn baselineRawJson = GeneratedColumn( - 'baseline_raw_json', + late final GeneratedColumn semanticHash = GeneratedColumn( + 'semantic_hash', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _attemptCountMeta = const VerificationMeta( - 'attemptCount', + static const VerificationMeta _serverDeletedMeta = const VerificationMeta( + 'serverDeleted', ); @override - late final GeneratedColumn attemptCount = GeneratedColumn( - 'attempt_count', + late final GeneratedColumn serverDeleted = GeneratedColumn( + 'server_deleted', aliasedName, false, - type: DriftSqlType.int, + type: DriftSqlType.bool, requiredDuringInsert: false, - defaultValue: const Constant(0), - ); - static const VerificationMeta _nextAttemptAtUtcMeta = const VerificationMeta( - 'nextAttemptAtUtc', + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("server_deleted" IN (0, 1))', + ), + defaultValue: const Constant(false), ); + static const VerificationMeta _baselineGenerationMeta = + const VerificationMeta('baselineGeneration'); @override - late final GeneratedColumn nextAttemptAtUtc = GeneratedColumn( - 'next_attempt_at_utc', + late final GeneratedColumn baselineGeneration = GeneratedColumn( + 'baseline_generation', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.int, requiredDuringInsert: false, + defaultValue: const Constant(0), ); - static const VerificationMeta _lastErrorCodeMeta = const VerificationMeta( - 'lastErrorCode', + static const VerificationMeta _firstSeenAtUtcMeta = const VerificationMeta( + 'firstSeenAtUtc', ); @override - late final GeneratedColumn lastErrorCode = GeneratedColumn( - 'last_error_code', + late final GeneratedColumn firstSeenAtUtc = GeneratedColumn( + 'first_seen_at_utc', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _lastErrorMessageMeta = const VerificationMeta( - 'lastErrorMessage', + requiredDuringInsert: true, ); - @override - late final GeneratedColumn lastErrorMessage = GeneratedColumn( - 'last_error_message', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, + static const VerificationMeta _lastFetchedAtUtcMeta = const VerificationMeta( + 'lastFetchedAtUtc', ); - static const VerificationMeta _stateMeta = const VerificationMeta('state'); @override - late final GeneratedColumn state = GeneratedColumn( - 'state', + late final GeneratedColumn lastFetchedAtUtc = GeneratedColumn( + 'last_fetched_at_utc', aliasedName, false, type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('pending'), + requiredDuringInsert: true, ); - static const VerificationMeta _lastErrorMeta = const VerificationMeta( - 'lastError', + static const VerificationMeta _lastChangedAtUtcMeta = const VerificationMeta( + 'lastChangedAtUtc', ); @override - late final GeneratedColumn lastError = GeneratedColumn( - 'last_error', + late final GeneratedColumn lastChangedAtUtc = GeneratedColumn( + 'last_changed_at_utc', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _createdAtUtcMeta = const VerificationMeta( - 'createdAtUtc', + static const VerificationMeta _lastParseStatusMeta = const VerificationMeta( + 'lastParseStatus', ); @override - late final GeneratedColumn createdAtUtc = GeneratedColumn( - 'created_at_utc', + late final GeneratedColumn lastParseStatus = GeneratedColumn( + 'last_parse_status', aliasedName, false, type: DriftSqlType.string, - requiredDuringInsert: true, + requiredDuringInsert: false, + defaultValue: const Constant('unparsed'), ); - static const VerificationMeta _updatedAtUtcMeta = const VerificationMeta( - 'updatedAtUtc', + static const VerificationMeta _lastParseErrorCodeMeta = + const VerificationMeta('lastParseErrorCode'); + @override + late final GeneratedColumn lastParseErrorCode = + GeneratedColumn( + 'last_parse_error_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _parserVersionMeta = const VerificationMeta( + 'parserVersion', ); @override - late final GeneratedColumn updatedAtUtc = GeneratedColumn( - 'updated_at_utc', + late final GeneratedColumn parserVersion = GeneratedColumn( + 'parser_version', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), ); @override List get $columns => [ id, accountId, - provider, - entityType, - operation, - operationType, - taskListId, - taskId, - calendarSourceId, - providerCalendarId, - eventId, - localTempId, - dependsOnOpId, - requestJson, - baselineUpdatedUtc, - baselineRawJson, - attemptCount, - nextAttemptAtUtc, - lastErrorCode, - lastErrorMessage, - state, - lastError, - createdAtUtc, - updatedAtUtc, + collectionId, + hrefKey, + requestUri, + etag, + contentType, + dominantComponentType, + componentMask, + primaryUid, + rawIcsBody, + rawBodyHash, + semanticHash, + serverDeleted, + baselineGeneration, + firstSeenAtUtc, + lastFetchedAtUtc, + lastChangedAtUtc, + lastParseStatus, + lastParseErrorCode, + parserVersion, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'pending_ops'; + static const String $name = 'dav_objects'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); @@ -5055,192 +4392,180 @@ class $PendingOpsTable extends PendingOps } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('provider')) { + if (data.containsKey('collection_id')) { context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + _collectionIdMeta, + collectionId.isAcceptableOrUnknown( + data['collection_id']!, + _collectionIdMeta, + ), ); + } else if (isInserting) { + context.missing(_collectionIdMeta); } - if (data.containsKey('entity_type')) { + if (data.containsKey('href_key')) { context.handle( - _entityTypeMeta, - entityType.isAcceptableOrUnknown(data['entity_type']!, _entityTypeMeta), + _hrefKeyMeta, + hrefKey.isAcceptableOrUnknown(data['href_key']!, _hrefKeyMeta), ); } else if (isInserting) { - context.missing(_entityTypeMeta); + context.missing(_hrefKeyMeta); } - if (data.containsKey('operation')) { + if (data.containsKey('request_uri')) { context.handle( - _operationMeta, - operation.isAcceptableOrUnknown(data['operation']!, _operationMeta), + _requestUriMeta, + requestUri.isAcceptableOrUnknown(data['request_uri']!, _requestUriMeta), ); } else if (isInserting) { - context.missing(_operationMeta); + context.missing(_requestUriMeta); } - if (data.containsKey('operation_type')) { + if (data.containsKey('etag')) { context.handle( - _operationTypeMeta, - operationType.isAcceptableOrUnknown( - data['operation_type']!, - _operationTypeMeta, - ), + _etagMeta, + etag.isAcceptableOrUnknown(data['etag']!, _etagMeta), ); } - if (data.containsKey('task_list_id')) { + if (data.containsKey('content_type')) { context.handle( - _taskListIdMeta, - taskListId.isAcceptableOrUnknown( - data['task_list_id']!, - _taskListIdMeta, + _contentTypeMeta, + contentType.isAcceptableOrUnknown( + data['content_type']!, + _contentTypeMeta, ), ); } - if (data.containsKey('task_id')) { + if (data.containsKey('dominant_component_type')) { context.handle( - _taskIdMeta, - taskId.isAcceptableOrUnknown(data['task_id']!, _taskIdMeta), + _dominantComponentTypeMeta, + dominantComponentType.isAcceptableOrUnknown( + data['dominant_component_type']!, + _dominantComponentTypeMeta, + ), ); } - if (data.containsKey('calendar_source_id')) { + if (data.containsKey('component_mask')) { context.handle( - _calendarSourceIdMeta, - calendarSourceId.isAcceptableOrUnknown( - data['calendar_source_id']!, - _calendarSourceIdMeta, + _componentMaskMeta, + componentMask.isAcceptableOrUnknown( + data['component_mask']!, + _componentMaskMeta, ), ); } - if (data.containsKey('provider_calendar_id')) { + if (data.containsKey('primary_uid')) { context.handle( - _providerCalendarIdMeta, - providerCalendarId.isAcceptableOrUnknown( - data['provider_calendar_id']!, - _providerCalendarIdMeta, - ), - ); - } - if (data.containsKey('event_id')) { - context.handle( - _eventIdMeta, - eventId.isAcceptableOrUnknown(data['event_id']!, _eventIdMeta), - ); - } - if (data.containsKey('local_temp_id')) { - context.handle( - _localTempIdMeta, - localTempId.isAcceptableOrUnknown( - data['local_temp_id']!, - _localTempIdMeta, - ), + _primaryUidMeta, + primaryUid.isAcceptableOrUnknown(data['primary_uid']!, _primaryUidMeta), ); } - if (data.containsKey('depends_on_op_id')) { + if (data.containsKey('raw_ics_body')) { context.handle( - _dependsOnOpIdMeta, - dependsOnOpId.isAcceptableOrUnknown( - data['depends_on_op_id']!, - _dependsOnOpIdMeta, + _rawIcsBodyMeta, + rawIcsBody.isAcceptableOrUnknown( + data['raw_ics_body']!, + _rawIcsBodyMeta, ), ); + } else if (isInserting) { + context.missing(_rawIcsBodyMeta); } - if (data.containsKey('request_json')) { + if (data.containsKey('raw_body_hash')) { context.handle( - _requestJsonMeta, - requestJson.isAcceptableOrUnknown( - data['request_json']!, - _requestJsonMeta, + _rawBodyHashMeta, + rawBodyHash.isAcceptableOrUnknown( + data['raw_body_hash']!, + _rawBodyHashMeta, ), ); } else if (isInserting) { - context.missing(_requestJsonMeta); + context.missing(_rawBodyHashMeta); } - if (data.containsKey('baseline_updated_utc')) { + if (data.containsKey('semantic_hash')) { context.handle( - _baselineUpdatedUtcMeta, - baselineUpdatedUtc.isAcceptableOrUnknown( - data['baseline_updated_utc']!, - _baselineUpdatedUtcMeta, + _semanticHashMeta, + semanticHash.isAcceptableOrUnknown( + data['semantic_hash']!, + _semanticHashMeta, ), ); } - if (data.containsKey('baseline_raw_json')) { + if (data.containsKey('server_deleted')) { context.handle( - _baselineRawJsonMeta, - baselineRawJson.isAcceptableOrUnknown( - data['baseline_raw_json']!, - _baselineRawJsonMeta, + _serverDeletedMeta, + serverDeleted.isAcceptableOrUnknown( + data['server_deleted']!, + _serverDeletedMeta, ), ); } - if (data.containsKey('attempt_count')) { + if (data.containsKey('baseline_generation')) { context.handle( - _attemptCountMeta, - attemptCount.isAcceptableOrUnknown( - data['attempt_count']!, - _attemptCountMeta, + _baselineGenerationMeta, + baselineGeneration.isAcceptableOrUnknown( + data['baseline_generation']!, + _baselineGenerationMeta, ), ); } - if (data.containsKey('next_attempt_at_utc')) { + if (data.containsKey('first_seen_at_utc')) { context.handle( - _nextAttemptAtUtcMeta, - nextAttemptAtUtc.isAcceptableOrUnknown( - data['next_attempt_at_utc']!, - _nextAttemptAtUtcMeta, + _firstSeenAtUtcMeta, + firstSeenAtUtc.isAcceptableOrUnknown( + data['first_seen_at_utc']!, + _firstSeenAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_firstSeenAtUtcMeta); } - if (data.containsKey('last_error_code')) { + if (data.containsKey('last_fetched_at_utc')) { context.handle( - _lastErrorCodeMeta, - lastErrorCode.isAcceptableOrUnknown( - data['last_error_code']!, - _lastErrorCodeMeta, + _lastFetchedAtUtcMeta, + lastFetchedAtUtc.isAcceptableOrUnknown( + data['last_fetched_at_utc']!, + _lastFetchedAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_lastFetchedAtUtcMeta); } - if (data.containsKey('last_error_message')) { + if (data.containsKey('last_changed_at_utc')) { context.handle( - _lastErrorMessageMeta, - lastErrorMessage.isAcceptableOrUnknown( - data['last_error_message']!, - _lastErrorMessageMeta, + _lastChangedAtUtcMeta, + lastChangedAtUtc.isAcceptableOrUnknown( + data['last_changed_at_utc']!, + _lastChangedAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_lastChangedAtUtcMeta); } - if (data.containsKey('state')) { - context.handle( - _stateMeta, - state.isAcceptableOrUnknown(data['state']!, _stateMeta), - ); - } - if (data.containsKey('last_error')) { + if (data.containsKey('last_parse_status')) { context.handle( - _lastErrorMeta, - lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + _lastParseStatusMeta, + lastParseStatus.isAcceptableOrUnknown( + data['last_parse_status']!, + _lastParseStatusMeta, + ), ); } - if (data.containsKey('created_at_utc')) { + if (data.containsKey('last_parse_error_code')) { context.handle( - _createdAtUtcMeta, - createdAtUtc.isAcceptableOrUnknown( - data['created_at_utc']!, - _createdAtUtcMeta, + _lastParseErrorCodeMeta, + lastParseErrorCode.isAcceptableOrUnknown( + data['last_parse_error_code']!, + _lastParseErrorCodeMeta, ), ); - } else if (isInserting) { - context.missing(_createdAtUtcMeta); } - if (data.containsKey('updated_at_utc')) { + if (data.containsKey('parser_version')) { context.handle( - _updatedAtUtcMeta, - updatedAtUtc.isAcceptableOrUnknown( - data['updated_at_utc']!, - _updatedAtUtcMeta, + _parserVersionMeta, + parserVersion.isAcceptableOrUnknown( + data['parser_version']!, + _parserVersionMeta, ), ); - } else if (isInserting) { - context.missing(_updatedAtUtcMeta); } return context; } @@ -5248,9 +4573,9 @@ class $PendingOpsTable extends PendingOps @override Set get $primaryKey => {id}; @override - PendingOp map(Map data, {String? tablePrefix}) { + DavObject map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PendingOp( + return DavObject( id: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}id'], @@ -5259,307 +4584,242 @@ class $PendingOpsTable extends PendingOps DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - ), - entityType: attachedDatabase.typeMapping.read( + collectionId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}entity_type'], + data['${effectivePrefix}collection_id'], )!, - operation: attachedDatabase.typeMapping.read( + hrefKey: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}operation'], + data['${effectivePrefix}href_key'], )!, - operationType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}operation_type'], - ), - taskListId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}task_list_id'], - ), - taskId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}task_id'], - ), - calendarSourceId: attachedDatabase.typeMapping.read( + requestUri: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}calendar_source_id'], - ), - providerCalendarId: attachedDatabase.typeMapping.read( + data['${effectivePrefix}request_uri'], + )!, + etag: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_calendar_id'], + data['${effectivePrefix}etag'], ), - eventId: attachedDatabase.typeMapping.read( + contentType: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}event_id'], + data['${effectivePrefix}content_type'], ), - localTempId: attachedDatabase.typeMapping.read( + dominantComponentType: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}local_temp_id'], + data['${effectivePrefix}dominant_component_type'], ), - dependsOnOpId: attachedDatabase.typeMapping.read( + componentMask: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}component_mask'], + )!, + primaryUid: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}depends_on_op_id'], + data['${effectivePrefix}primary_uid'], ), - requestJson: attachedDatabase.typeMapping.read( + rawIcsBody: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}request_json'], + data['${effectivePrefix}raw_ics_body'], )!, - baselineUpdatedUtc: attachedDatabase.typeMapping.read( + rawBodyHash: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}baseline_updated_utc'], - ), - baselineRawJson: attachedDatabase.typeMapping.read( + data['${effectivePrefix}raw_body_hash'], + )!, + semanticHash: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}baseline_raw_json'], + data['${effectivePrefix}semantic_hash'], ), - attemptCount: attachedDatabase.typeMapping.read( + serverDeleted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}server_deleted'], + )!, + baselineGeneration: attachedDatabase.typeMapping.read( DriftSqlType.int, - data['${effectivePrefix}attempt_count'], + data['${effectivePrefix}baseline_generation'], )!, - nextAttemptAtUtc: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}next_attempt_at_utc'], - ), - lastErrorCode: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}last_error_code'], - ), - lastErrorMessage: attachedDatabase.typeMapping.read( + firstSeenAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}last_error_message'], - ), - state: attachedDatabase.typeMapping.read( + data['${effectivePrefix}first_seen_at_utc'], + )!, + lastFetchedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}state'], + data['${effectivePrefix}last_fetched_at_utc'], )!, - lastError: attachedDatabase.typeMapping.read( + lastChangedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}last_error'], - ), - createdAtUtc: attachedDatabase.typeMapping.read( + data['${effectivePrefix}last_changed_at_utc'], + )!, + lastParseStatus: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}created_at_utc'], + data['${effectivePrefix}last_parse_status'], )!, - updatedAtUtc: attachedDatabase.typeMapping.read( + lastParseErrorCode: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_at_utc'], + data['${effectivePrefix}last_parse_error_code'], + ), + parserVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}parser_version'], )!, ); } @override - $PendingOpsTable createAlias(String alias) { - return $PendingOpsTable(attachedDatabase, alias); + $DavObjectsTable createAlias(String alias) { + return $DavObjectsTable(attachedDatabase, alias); } } -class PendingOp extends DataClass implements Insertable { +class DavObject extends DataClass implements Insertable { final String id; final String accountId; - final String? provider; - final String entityType; - final String operation; - final String? operationType; - final String? taskListId; - final String? taskId; - final String? calendarSourceId; - final String? providerCalendarId; - final String? eventId; - final String? localTempId; - final String? dependsOnOpId; - final String requestJson; - final String? baselineUpdatedUtc; - final String? baselineRawJson; - final int attemptCount; - final String? nextAttemptAtUtc; - final String? lastErrorCode; - final String? lastErrorMessage; - final String state; - final String? lastError; - final String createdAtUtc; - final String updatedAtUtc; - const PendingOp({ + final String collectionId; + final String hrefKey; + final String requestUri; + final String? etag; + final String? contentType; + final String? dominantComponentType; + final int componentMask; + final String? primaryUid; + final String rawIcsBody; + final String rawBodyHash; + final String? semanticHash; + final bool serverDeleted; + final int baselineGeneration; + final String firstSeenAtUtc; + final String lastFetchedAtUtc; + final String lastChangedAtUtc; + final String lastParseStatus; + final String? lastParseErrorCode; + final int parserVersion; + const DavObject({ required this.id, required this.accountId, - this.provider, - required this.entityType, - required this.operation, - this.operationType, - this.taskListId, - this.taskId, - this.calendarSourceId, - this.providerCalendarId, - this.eventId, - this.localTempId, - this.dependsOnOpId, - required this.requestJson, - this.baselineUpdatedUtc, - this.baselineRawJson, - required this.attemptCount, - this.nextAttemptAtUtc, - this.lastErrorCode, - this.lastErrorMessage, - required this.state, - this.lastError, - required this.createdAtUtc, - required this.updatedAtUtc, + required this.collectionId, + required this.hrefKey, + required this.requestUri, + this.etag, + this.contentType, + this.dominantComponentType, + required this.componentMask, + this.primaryUid, + required this.rawIcsBody, + required this.rawBodyHash, + this.semanticHash, + required this.serverDeleted, + required this.baselineGeneration, + required this.firstSeenAtUtc, + required this.lastFetchedAtUtc, + required this.lastChangedAtUtc, + required this.lastParseStatus, + this.lastParseErrorCode, + required this.parserVersion, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['account_id'] = Variable(accountId); - if (!nullToAbsent || provider != null) { - map['provider'] = Variable(provider); - } - map['entity_type'] = Variable(entityType); - map['operation'] = Variable(operation); - if (!nullToAbsent || operationType != null) { - map['operation_type'] = Variable(operationType); - } - if (!nullToAbsent || taskListId != null) { - map['task_list_id'] = Variable(taskListId); - } - if (!nullToAbsent || taskId != null) { - map['task_id'] = Variable(taskId); - } - if (!nullToAbsent || calendarSourceId != null) { - map['calendar_source_id'] = Variable(calendarSourceId); - } - if (!nullToAbsent || providerCalendarId != null) { - map['provider_calendar_id'] = Variable(providerCalendarId); - } - if (!nullToAbsent || eventId != null) { - map['event_id'] = Variable(eventId); - } - if (!nullToAbsent || localTempId != null) { - map['local_temp_id'] = Variable(localTempId); - } - if (!nullToAbsent || dependsOnOpId != null) { - map['depends_on_op_id'] = Variable(dependsOnOpId); + map['collection_id'] = Variable(collectionId); + map['href_key'] = Variable(hrefKey); + map['request_uri'] = Variable(requestUri); + if (!nullToAbsent || etag != null) { + map['etag'] = Variable(etag); } - map['request_json'] = Variable(requestJson); - if (!nullToAbsent || baselineUpdatedUtc != null) { - map['baseline_updated_utc'] = Variable(baselineUpdatedUtc); + if (!nullToAbsent || contentType != null) { + map['content_type'] = Variable(contentType); } - if (!nullToAbsent || baselineRawJson != null) { - map['baseline_raw_json'] = Variable(baselineRawJson); + if (!nullToAbsent || dominantComponentType != null) { + map['dominant_component_type'] = Variable(dominantComponentType); } - map['attempt_count'] = Variable(attemptCount); - if (!nullToAbsent || nextAttemptAtUtc != null) { - map['next_attempt_at_utc'] = Variable(nextAttemptAtUtc); + map['component_mask'] = Variable(componentMask); + if (!nullToAbsent || primaryUid != null) { + map['primary_uid'] = Variable(primaryUid); } - if (!nullToAbsent || lastErrorCode != null) { - map['last_error_code'] = Variable(lastErrorCode); + map['raw_ics_body'] = Variable(rawIcsBody); + map['raw_body_hash'] = Variable(rawBodyHash); + if (!nullToAbsent || semanticHash != null) { + map['semantic_hash'] = Variable(semanticHash); } - if (!nullToAbsent || lastErrorMessage != null) { - map['last_error_message'] = Variable(lastErrorMessage); + map['server_deleted'] = Variable(serverDeleted); + map['baseline_generation'] = Variable(baselineGeneration); + map['first_seen_at_utc'] = Variable(firstSeenAtUtc); + map['last_fetched_at_utc'] = Variable(lastFetchedAtUtc); + map['last_changed_at_utc'] = Variable(lastChangedAtUtc); + map['last_parse_status'] = Variable(lastParseStatus); + if (!nullToAbsent || lastParseErrorCode != null) { + map['last_parse_error_code'] = Variable(lastParseErrorCode); } - map['state'] = Variable(state); - if (!nullToAbsent || lastError != null) { - map['last_error'] = Variable(lastError); - } - map['created_at_utc'] = Variable(createdAtUtc); - map['updated_at_utc'] = Variable(updatedAtUtc); + map['parser_version'] = Variable(parserVersion); return map; } - PendingOpsCompanion toCompanion(bool nullToAbsent) { - return PendingOpsCompanion( + DavObjectsCompanion toCompanion(bool nullToAbsent) { + return DavObjectsCompanion( id: Value(id), accountId: Value(accountId), - provider: provider == null && nullToAbsent - ? const Value.absent() - : Value(provider), - entityType: Value(entityType), - operation: Value(operation), - operationType: operationType == null && nullToAbsent - ? const Value.absent() - : Value(operationType), - taskListId: taskListId == null && nullToAbsent - ? const Value.absent() - : Value(taskListId), - taskId: taskId == null && nullToAbsent - ? const Value.absent() - : Value(taskId), - calendarSourceId: calendarSourceId == null && nullToAbsent - ? const Value.absent() - : Value(calendarSourceId), - providerCalendarId: providerCalendarId == null && nullToAbsent - ? const Value.absent() - : Value(providerCalendarId), - eventId: eventId == null && nullToAbsent - ? const Value.absent() - : Value(eventId), - localTempId: localTempId == null && nullToAbsent - ? const Value.absent() - : Value(localTempId), - dependsOnOpId: dependsOnOpId == null && nullToAbsent - ? const Value.absent() - : Value(dependsOnOpId), - requestJson: Value(requestJson), - baselineUpdatedUtc: baselineUpdatedUtc == null && nullToAbsent - ? const Value.absent() - : Value(baselineUpdatedUtc), - baselineRawJson: baselineRawJson == null && nullToAbsent + collectionId: Value(collectionId), + hrefKey: Value(hrefKey), + requestUri: Value(requestUri), + etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag), + contentType: contentType == null && nullToAbsent ? const Value.absent() - : Value(baselineRawJson), - attemptCount: Value(attemptCount), - nextAttemptAtUtc: nextAttemptAtUtc == null && nullToAbsent + : Value(contentType), + dominantComponentType: dominantComponentType == null && nullToAbsent ? const Value.absent() - : Value(nextAttemptAtUtc), - lastErrorCode: lastErrorCode == null && nullToAbsent + : Value(dominantComponentType), + componentMask: Value(componentMask), + primaryUid: primaryUid == null && nullToAbsent ? const Value.absent() - : Value(lastErrorCode), - lastErrorMessage: lastErrorMessage == null && nullToAbsent + : Value(primaryUid), + rawIcsBody: Value(rawIcsBody), + rawBodyHash: Value(rawBodyHash), + semanticHash: semanticHash == null && nullToAbsent ? const Value.absent() - : Value(lastErrorMessage), - state: Value(state), - lastError: lastError == null && nullToAbsent + : Value(semanticHash), + serverDeleted: Value(serverDeleted), + baselineGeneration: Value(baselineGeneration), + firstSeenAtUtc: Value(firstSeenAtUtc), + lastFetchedAtUtc: Value(lastFetchedAtUtc), + lastChangedAtUtc: Value(lastChangedAtUtc), + lastParseStatus: Value(lastParseStatus), + lastParseErrorCode: lastParseErrorCode == null && nullToAbsent ? const Value.absent() - : Value(lastError), - createdAtUtc: Value(createdAtUtc), - updatedAtUtc: Value(updatedAtUtc), + : Value(lastParseErrorCode), + parserVersion: Value(parserVersion), ); } - factory PendingOp.fromJson( + factory DavObject.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return PendingOp( + return DavObject( id: serializer.fromJson(json['id']), accountId: serializer.fromJson(json['accountId']), - provider: serializer.fromJson(json['provider']), - entityType: serializer.fromJson(json['entityType']), - operation: serializer.fromJson(json['operation']), - operationType: serializer.fromJson(json['operationType']), - taskListId: serializer.fromJson(json['taskListId']), - taskId: serializer.fromJson(json['taskId']), - calendarSourceId: serializer.fromJson(json['calendarSourceId']), - providerCalendarId: serializer.fromJson( - json['providerCalendarId'], + collectionId: serializer.fromJson(json['collectionId']), + hrefKey: serializer.fromJson(json['hrefKey']), + requestUri: serializer.fromJson(json['requestUri']), + etag: serializer.fromJson(json['etag']), + contentType: serializer.fromJson(json['contentType']), + dominantComponentType: serializer.fromJson( + json['dominantComponentType'], ), - eventId: serializer.fromJson(json['eventId']), - localTempId: serializer.fromJson(json['localTempId']), - dependsOnOpId: serializer.fromJson(json['dependsOnOpId']), - requestJson: serializer.fromJson(json['requestJson']), - baselineUpdatedUtc: serializer.fromJson( - json['baselineUpdatedUtc'], + componentMask: serializer.fromJson(json['componentMask']), + primaryUid: serializer.fromJson(json['primaryUid']), + rawIcsBody: serializer.fromJson(json['rawIcsBody']), + rawBodyHash: serializer.fromJson(json['rawBodyHash']), + semanticHash: serializer.fromJson(json['semanticHash']), + serverDeleted: serializer.fromJson(json['serverDeleted']), + baselineGeneration: serializer.fromJson(json['baselineGeneration']), + firstSeenAtUtc: serializer.fromJson(json['firstSeenAtUtc']), + lastFetchedAtUtc: serializer.fromJson(json['lastFetchedAtUtc']), + lastChangedAtUtc: serializer.fromJson(json['lastChangedAtUtc']), + lastParseStatus: serializer.fromJson(json['lastParseStatus']), + lastParseErrorCode: serializer.fromJson( + json['lastParseErrorCode'], ), - baselineRawJson: serializer.fromJson(json['baselineRawJson']), - attemptCount: serializer.fromJson(json['attemptCount']), - nextAttemptAtUtc: serializer.fromJson(json['nextAttemptAtUtc']), - lastErrorCode: serializer.fromJson(json['lastErrorCode']), - lastErrorMessage: serializer.fromJson(json['lastErrorMessage']), - state: serializer.fromJson(json['state']), - lastError: serializer.fromJson(json['lastError']), - createdAtUtc: serializer.fromJson(json['createdAtUtc']), - updatedAtUtc: serializer.fromJson(json['updatedAtUtc']), + parserVersion: serializer.fromJson(json['parserVersion']), ); } @override @@ -5568,188 +4828,163 @@ class PendingOp extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'accountId': serializer.toJson(accountId), - 'provider': serializer.toJson(provider), - 'entityType': serializer.toJson(entityType), - 'operation': serializer.toJson(operation), - 'operationType': serializer.toJson(operationType), - 'taskListId': serializer.toJson(taskListId), - 'taskId': serializer.toJson(taskId), - 'calendarSourceId': serializer.toJson(calendarSourceId), - 'providerCalendarId': serializer.toJson(providerCalendarId), - 'eventId': serializer.toJson(eventId), - 'localTempId': serializer.toJson(localTempId), - 'dependsOnOpId': serializer.toJson(dependsOnOpId), - 'requestJson': serializer.toJson(requestJson), - 'baselineUpdatedUtc': serializer.toJson(baselineUpdatedUtc), - 'baselineRawJson': serializer.toJson(baselineRawJson), - 'attemptCount': serializer.toJson(attemptCount), - 'nextAttemptAtUtc': serializer.toJson(nextAttemptAtUtc), - 'lastErrorCode': serializer.toJson(lastErrorCode), - 'lastErrorMessage': serializer.toJson(lastErrorMessage), - 'state': serializer.toJson(state), - 'lastError': serializer.toJson(lastError), - 'createdAtUtc': serializer.toJson(createdAtUtc), - 'updatedAtUtc': serializer.toJson(updatedAtUtc), + 'collectionId': serializer.toJson(collectionId), + 'hrefKey': serializer.toJson(hrefKey), + 'requestUri': serializer.toJson(requestUri), + 'etag': serializer.toJson(etag), + 'contentType': serializer.toJson(contentType), + 'dominantComponentType': serializer.toJson( + dominantComponentType, + ), + 'componentMask': serializer.toJson(componentMask), + 'primaryUid': serializer.toJson(primaryUid), + 'rawIcsBody': serializer.toJson(rawIcsBody), + 'rawBodyHash': serializer.toJson(rawBodyHash), + 'semanticHash': serializer.toJson(semanticHash), + 'serverDeleted': serializer.toJson(serverDeleted), + 'baselineGeneration': serializer.toJson(baselineGeneration), + 'firstSeenAtUtc': serializer.toJson(firstSeenAtUtc), + 'lastFetchedAtUtc': serializer.toJson(lastFetchedAtUtc), + 'lastChangedAtUtc': serializer.toJson(lastChangedAtUtc), + 'lastParseStatus': serializer.toJson(lastParseStatus), + 'lastParseErrorCode': serializer.toJson(lastParseErrorCode), + 'parserVersion': serializer.toJson(parserVersion), }; } - PendingOp copyWith({ + DavObject copyWith({ String? id, String? accountId, - Value provider = const Value.absent(), - String? entityType, - String? operation, - Value operationType = const Value.absent(), - Value taskListId = const Value.absent(), - Value taskId = const Value.absent(), - Value calendarSourceId = const Value.absent(), - Value providerCalendarId = const Value.absent(), - Value eventId = const Value.absent(), - Value localTempId = const Value.absent(), - Value dependsOnOpId = const Value.absent(), - String? requestJson, - Value baselineUpdatedUtc = const Value.absent(), - Value baselineRawJson = const Value.absent(), - int? attemptCount, - Value nextAttemptAtUtc = const Value.absent(), - Value lastErrorCode = const Value.absent(), - Value lastErrorMessage = const Value.absent(), - String? state, - Value lastError = const Value.absent(), - String? createdAtUtc, - String? updatedAtUtc, - }) => PendingOp( + String? collectionId, + String? hrefKey, + String? requestUri, + Value etag = const Value.absent(), + Value contentType = const Value.absent(), + Value dominantComponentType = const Value.absent(), + int? componentMask, + Value primaryUid = const Value.absent(), + String? rawIcsBody, + String? rawBodyHash, + Value semanticHash = const Value.absent(), + bool? serverDeleted, + int? baselineGeneration, + String? firstSeenAtUtc, + String? lastFetchedAtUtc, + String? lastChangedAtUtc, + String? lastParseStatus, + Value lastParseErrorCode = const Value.absent(), + int? parserVersion, + }) => DavObject( id: id ?? this.id, accountId: accountId ?? this.accountId, - provider: provider.present ? provider.value : this.provider, - entityType: entityType ?? this.entityType, - operation: operation ?? this.operation, - operationType: operationType.present - ? operationType.value - : this.operationType, - taskListId: taskListId.present ? taskListId.value : this.taskListId, - taskId: taskId.present ? taskId.value : this.taskId, - calendarSourceId: calendarSourceId.present - ? calendarSourceId.value - : this.calendarSourceId, - providerCalendarId: providerCalendarId.present - ? providerCalendarId.value - : this.providerCalendarId, - eventId: eventId.present ? eventId.value : this.eventId, - localTempId: localTempId.present ? localTempId.value : this.localTempId, - dependsOnOpId: dependsOnOpId.present - ? dependsOnOpId.value - : this.dependsOnOpId, - requestJson: requestJson ?? this.requestJson, - baselineUpdatedUtc: baselineUpdatedUtc.present - ? baselineUpdatedUtc.value - : this.baselineUpdatedUtc, - baselineRawJson: baselineRawJson.present - ? baselineRawJson.value - : this.baselineRawJson, - attemptCount: attemptCount ?? this.attemptCount, - nextAttemptAtUtc: nextAttemptAtUtc.present - ? nextAttemptAtUtc.value - : this.nextAttemptAtUtc, - lastErrorCode: lastErrorCode.present - ? lastErrorCode.value - : this.lastErrorCode, - lastErrorMessage: lastErrorMessage.present - ? lastErrorMessage.value - : this.lastErrorMessage, - state: state ?? this.state, - lastError: lastError.present ? lastError.value : this.lastError, - createdAtUtc: createdAtUtc ?? this.createdAtUtc, - updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, - ); - PendingOp copyWithCompanion(PendingOpsCompanion data) { - return PendingOp( + collectionId: collectionId ?? this.collectionId, + hrefKey: hrefKey ?? this.hrefKey, + requestUri: requestUri ?? this.requestUri, + etag: etag.present ? etag.value : this.etag, + contentType: contentType.present ? contentType.value : this.contentType, + dominantComponentType: dominantComponentType.present + ? dominantComponentType.value + : this.dominantComponentType, + componentMask: componentMask ?? this.componentMask, + primaryUid: primaryUid.present ? primaryUid.value : this.primaryUid, + rawIcsBody: rawIcsBody ?? this.rawIcsBody, + rawBodyHash: rawBodyHash ?? this.rawBodyHash, + semanticHash: semanticHash.present ? semanticHash.value : this.semanticHash, + serverDeleted: serverDeleted ?? this.serverDeleted, + baselineGeneration: baselineGeneration ?? this.baselineGeneration, + firstSeenAtUtc: firstSeenAtUtc ?? this.firstSeenAtUtc, + lastFetchedAtUtc: lastFetchedAtUtc ?? this.lastFetchedAtUtc, + lastChangedAtUtc: lastChangedAtUtc ?? this.lastChangedAtUtc, + lastParseStatus: lastParseStatus ?? this.lastParseStatus, + lastParseErrorCode: lastParseErrorCode.present + ? lastParseErrorCode.value + : this.lastParseErrorCode, + parserVersion: parserVersion ?? this.parserVersion, + ); + DavObject copyWithCompanion(DavObjectsCompanion data) { + return DavObject( id: data.id.present ? data.id.value : this.id, accountId: data.accountId.present ? data.accountId.value : this.accountId, - provider: data.provider.present ? data.provider.value : this.provider, - entityType: data.entityType.present - ? data.entityType.value - : this.entityType, - operation: data.operation.present ? data.operation.value : this.operation, - operationType: data.operationType.present - ? data.operationType.value - : this.operationType, - taskListId: data.taskListId.present - ? data.taskListId.value - : this.taskListId, - taskId: data.taskId.present ? data.taskId.value : this.taskId, - calendarSourceId: data.calendarSourceId.present - ? data.calendarSourceId.value - : this.calendarSourceId, - providerCalendarId: data.providerCalendarId.present - ? data.providerCalendarId.value - : this.providerCalendarId, - eventId: data.eventId.present ? data.eventId.value : this.eventId, - localTempId: data.localTempId.present - ? data.localTempId.value - : this.localTempId, - dependsOnOpId: data.dependsOnOpId.present - ? data.dependsOnOpId.value - : this.dependsOnOpId, - requestJson: data.requestJson.present - ? data.requestJson.value - : this.requestJson, - baselineUpdatedUtc: data.baselineUpdatedUtc.present - ? data.baselineUpdatedUtc.value - : this.baselineUpdatedUtc, - baselineRawJson: data.baselineRawJson.present - ? data.baselineRawJson.value - : this.baselineRawJson, - attemptCount: data.attemptCount.present - ? data.attemptCount.value - : this.attemptCount, - nextAttemptAtUtc: data.nextAttemptAtUtc.present - ? data.nextAttemptAtUtc.value - : this.nextAttemptAtUtc, - lastErrorCode: data.lastErrorCode.present - ? data.lastErrorCode.value - : this.lastErrorCode, - lastErrorMessage: data.lastErrorMessage.present - ? data.lastErrorMessage.value - : this.lastErrorMessage, - state: data.state.present ? data.state.value : this.state, - lastError: data.lastError.present ? data.lastError.value : this.lastError, - createdAtUtc: data.createdAtUtc.present - ? data.createdAtUtc.value - : this.createdAtUtc, - updatedAtUtc: data.updatedAtUtc.present - ? data.updatedAtUtc.value - : this.updatedAtUtc, + collectionId: data.collectionId.present + ? data.collectionId.value + : this.collectionId, + hrefKey: data.hrefKey.present ? data.hrefKey.value : this.hrefKey, + requestUri: data.requestUri.present + ? data.requestUri.value + : this.requestUri, + etag: data.etag.present ? data.etag.value : this.etag, + contentType: data.contentType.present + ? data.contentType.value + : this.contentType, + dominantComponentType: data.dominantComponentType.present + ? data.dominantComponentType.value + : this.dominantComponentType, + componentMask: data.componentMask.present + ? data.componentMask.value + : this.componentMask, + primaryUid: data.primaryUid.present + ? data.primaryUid.value + : this.primaryUid, + rawIcsBody: data.rawIcsBody.present + ? data.rawIcsBody.value + : this.rawIcsBody, + rawBodyHash: data.rawBodyHash.present + ? data.rawBodyHash.value + : this.rawBodyHash, + semanticHash: data.semanticHash.present + ? data.semanticHash.value + : this.semanticHash, + serverDeleted: data.serverDeleted.present + ? data.serverDeleted.value + : this.serverDeleted, + baselineGeneration: data.baselineGeneration.present + ? data.baselineGeneration.value + : this.baselineGeneration, + firstSeenAtUtc: data.firstSeenAtUtc.present + ? data.firstSeenAtUtc.value + : this.firstSeenAtUtc, + lastFetchedAtUtc: data.lastFetchedAtUtc.present + ? data.lastFetchedAtUtc.value + : this.lastFetchedAtUtc, + lastChangedAtUtc: data.lastChangedAtUtc.present + ? data.lastChangedAtUtc.value + : this.lastChangedAtUtc, + lastParseStatus: data.lastParseStatus.present + ? data.lastParseStatus.value + : this.lastParseStatus, + lastParseErrorCode: data.lastParseErrorCode.present + ? data.lastParseErrorCode.value + : this.lastParseErrorCode, + parserVersion: data.parserVersion.present + ? data.parserVersion.value + : this.parserVersion, ); } @override String toString() { - return (StringBuffer('PendingOp(') + return (StringBuffer('DavObject(') ..write('id: $id, ') ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('entityType: $entityType, ') - ..write('operation: $operation, ') - ..write('operationType: $operationType, ') - ..write('taskListId: $taskListId, ') - ..write('taskId: $taskId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('eventId: $eventId, ') - ..write('localTempId: $localTempId, ') - ..write('dependsOnOpId: $dependsOnOpId, ') - ..write('requestJson: $requestJson, ') - ..write('baselineUpdatedUtc: $baselineUpdatedUtc, ') - ..write('baselineRawJson: $baselineRawJson, ') - ..write('attemptCount: $attemptCount, ') - ..write('nextAttemptAtUtc: $nextAttemptAtUtc, ') - ..write('lastErrorCode: $lastErrorCode, ') - ..write('lastErrorMessage: $lastErrorMessage, ') - ..write('state: $state, ') - ..write('lastError: $lastError, ') - ..write('createdAtUtc: $createdAtUtc, ') - ..write('updatedAtUtc: $updatedAtUtc') + ..write('collectionId: $collectionId, ') + ..write('hrefKey: $hrefKey, ') + ..write('requestUri: $requestUri, ') + ..write('etag: $etag, ') + ..write('contentType: $contentType, ') + ..write('dominantComponentType: $dominantComponentType, ') + ..write('componentMask: $componentMask, ') + ..write('primaryUid: $primaryUid, ') + ..write('rawIcsBody: $rawIcsBody, ') + ..write('rawBodyHash: $rawBodyHash, ') + ..write('semanticHash: $semanticHash, ') + ..write('serverDeleted: $serverDeleted, ') + ..write('baselineGeneration: $baselineGeneration, ') + ..write('firstSeenAtUtc: $firstSeenAtUtc, ') + ..write('lastFetchedAtUtc: $lastFetchedAtUtc, ') + ..write('lastChangedAtUtc: $lastChangedAtUtc, ') + ..write('lastParseStatus: $lastParseStatus, ') + ..write('lastParseErrorCode: $lastParseErrorCode, ') + ..write('parserVersion: $parserVersion') ..write(')')) .toString(); } @@ -5758,255 +4993,232 @@ class PendingOp extends DataClass implements Insertable { int get hashCode => Object.hashAll([ id, accountId, - provider, - entityType, - operation, - operationType, - taskListId, - taskId, - calendarSourceId, - providerCalendarId, - eventId, - localTempId, - dependsOnOpId, - requestJson, - baselineUpdatedUtc, - baselineRawJson, - attemptCount, - nextAttemptAtUtc, - lastErrorCode, - lastErrorMessage, - state, - lastError, - createdAtUtc, - updatedAtUtc, + collectionId, + hrefKey, + requestUri, + etag, + contentType, + dominantComponentType, + componentMask, + primaryUid, + rawIcsBody, + rawBodyHash, + semanticHash, + serverDeleted, + baselineGeneration, + firstSeenAtUtc, + lastFetchedAtUtc, + lastChangedAtUtc, + lastParseStatus, + lastParseErrorCode, + parserVersion, ]); @override bool operator ==(Object other) => identical(this, other) || - (other is PendingOp && + (other is DavObject && other.id == this.id && other.accountId == this.accountId && - other.provider == this.provider && - other.entityType == this.entityType && - other.operation == this.operation && - other.operationType == this.operationType && - other.taskListId == this.taskListId && - other.taskId == this.taskId && - other.calendarSourceId == this.calendarSourceId && - other.providerCalendarId == this.providerCalendarId && - other.eventId == this.eventId && - other.localTempId == this.localTempId && - other.dependsOnOpId == this.dependsOnOpId && - other.requestJson == this.requestJson && - other.baselineUpdatedUtc == this.baselineUpdatedUtc && - other.baselineRawJson == this.baselineRawJson && - other.attemptCount == this.attemptCount && - other.nextAttemptAtUtc == this.nextAttemptAtUtc && - other.lastErrorCode == this.lastErrorCode && - other.lastErrorMessage == this.lastErrorMessage && - other.state == this.state && - other.lastError == this.lastError && - other.createdAtUtc == this.createdAtUtc && - other.updatedAtUtc == this.updatedAtUtc); + other.collectionId == this.collectionId && + other.hrefKey == this.hrefKey && + other.requestUri == this.requestUri && + other.etag == this.etag && + other.contentType == this.contentType && + other.dominantComponentType == this.dominantComponentType && + other.componentMask == this.componentMask && + other.primaryUid == this.primaryUid && + other.rawIcsBody == this.rawIcsBody && + other.rawBodyHash == this.rawBodyHash && + other.semanticHash == this.semanticHash && + other.serverDeleted == this.serverDeleted && + other.baselineGeneration == this.baselineGeneration && + other.firstSeenAtUtc == this.firstSeenAtUtc && + other.lastFetchedAtUtc == this.lastFetchedAtUtc && + other.lastChangedAtUtc == this.lastChangedAtUtc && + other.lastParseStatus == this.lastParseStatus && + other.lastParseErrorCode == this.lastParseErrorCode && + other.parserVersion == this.parserVersion); } -class PendingOpsCompanion extends UpdateCompanion { +class DavObjectsCompanion extends UpdateCompanion { final Value id; final Value accountId; - final Value provider; - final Value entityType; - final Value operation; - final Value operationType; - final Value taskListId; - final Value taskId; - final Value calendarSourceId; - final Value providerCalendarId; - final Value eventId; - final Value localTempId; - final Value dependsOnOpId; - final Value requestJson; - final Value baselineUpdatedUtc; - final Value baselineRawJson; - final Value attemptCount; - final Value nextAttemptAtUtc; - final Value lastErrorCode; - final Value lastErrorMessage; - final Value state; - final Value lastError; - final Value createdAtUtc; - final Value updatedAtUtc; + final Value collectionId; + final Value hrefKey; + final Value requestUri; + final Value etag; + final Value contentType; + final Value dominantComponentType; + final Value componentMask; + final Value primaryUid; + final Value rawIcsBody; + final Value rawBodyHash; + final Value semanticHash; + final Value serverDeleted; + final Value baselineGeneration; + final Value firstSeenAtUtc; + final Value lastFetchedAtUtc; + final Value lastChangedAtUtc; + final Value lastParseStatus; + final Value lastParseErrorCode; + final Value parserVersion; final Value rowid; - const PendingOpsCompanion({ + const DavObjectsCompanion({ this.id = const Value.absent(), this.accountId = const Value.absent(), - this.provider = const Value.absent(), - this.entityType = const Value.absent(), - this.operation = const Value.absent(), - this.operationType = const Value.absent(), - this.taskListId = const Value.absent(), - this.taskId = const Value.absent(), - this.calendarSourceId = const Value.absent(), - this.providerCalendarId = const Value.absent(), - this.eventId = const Value.absent(), - this.localTempId = const Value.absent(), - this.dependsOnOpId = const Value.absent(), - this.requestJson = const Value.absent(), - this.baselineUpdatedUtc = const Value.absent(), - this.baselineRawJson = const Value.absent(), - this.attemptCount = const Value.absent(), - this.nextAttemptAtUtc = const Value.absent(), - this.lastErrorCode = const Value.absent(), - this.lastErrorMessage = const Value.absent(), - this.state = const Value.absent(), - this.lastError = const Value.absent(), - this.createdAtUtc = const Value.absent(), - this.updatedAtUtc = const Value.absent(), + this.collectionId = const Value.absent(), + this.hrefKey = const Value.absent(), + this.requestUri = const Value.absent(), + this.etag = const Value.absent(), + this.contentType = const Value.absent(), + this.dominantComponentType = const Value.absent(), + this.componentMask = const Value.absent(), + this.primaryUid = const Value.absent(), + this.rawIcsBody = const Value.absent(), + this.rawBodyHash = const Value.absent(), + this.semanticHash = const Value.absent(), + this.serverDeleted = const Value.absent(), + this.baselineGeneration = const Value.absent(), + this.firstSeenAtUtc = const Value.absent(), + this.lastFetchedAtUtc = const Value.absent(), + this.lastChangedAtUtc = const Value.absent(), + this.lastParseStatus = const Value.absent(), + this.lastParseErrorCode = const Value.absent(), + this.parserVersion = const Value.absent(), this.rowid = const Value.absent(), }); - PendingOpsCompanion.insert({ + DavObjectsCompanion.insert({ required String id, required String accountId, - this.provider = const Value.absent(), - required String entityType, - required String operation, - this.operationType = const Value.absent(), - this.taskListId = const Value.absent(), - this.taskId = const Value.absent(), - this.calendarSourceId = const Value.absent(), - this.providerCalendarId = const Value.absent(), - this.eventId = const Value.absent(), - this.localTempId = const Value.absent(), - this.dependsOnOpId = const Value.absent(), - required String requestJson, - this.baselineUpdatedUtc = const Value.absent(), - this.baselineRawJson = const Value.absent(), - this.attemptCount = const Value.absent(), - this.nextAttemptAtUtc = const Value.absent(), - this.lastErrorCode = const Value.absent(), - this.lastErrorMessage = const Value.absent(), - this.state = const Value.absent(), - this.lastError = const Value.absent(), - required String createdAtUtc, - required String updatedAtUtc, + required String collectionId, + required String hrefKey, + required String requestUri, + this.etag = const Value.absent(), + this.contentType = const Value.absent(), + this.dominantComponentType = const Value.absent(), + this.componentMask = const Value.absent(), + this.primaryUid = const Value.absent(), + required String rawIcsBody, + required String rawBodyHash, + this.semanticHash = const Value.absent(), + this.serverDeleted = const Value.absent(), + this.baselineGeneration = const Value.absent(), + required String firstSeenAtUtc, + required String lastFetchedAtUtc, + required String lastChangedAtUtc, + this.lastParseStatus = const Value.absent(), + this.lastParseErrorCode = const Value.absent(), + this.parserVersion = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), accountId = Value(accountId), - entityType = Value(entityType), - operation = Value(operation), - requestJson = Value(requestJson), - createdAtUtc = Value(createdAtUtc), - updatedAtUtc = Value(updatedAtUtc); - static Insertable custom({ + collectionId = Value(collectionId), + hrefKey = Value(hrefKey), + requestUri = Value(requestUri), + rawIcsBody = Value(rawIcsBody), + rawBodyHash = Value(rawBodyHash), + firstSeenAtUtc = Value(firstSeenAtUtc), + lastFetchedAtUtc = Value(lastFetchedAtUtc), + lastChangedAtUtc = Value(lastChangedAtUtc); + static Insertable custom({ Expression? id, Expression? accountId, - Expression? provider, - Expression? entityType, - Expression? operation, - Expression? operationType, - Expression? taskListId, - Expression? taskId, - Expression? calendarSourceId, - Expression? providerCalendarId, - Expression? eventId, - Expression? localTempId, - Expression? dependsOnOpId, - Expression? requestJson, - Expression? baselineUpdatedUtc, - Expression? baselineRawJson, - Expression? attemptCount, - Expression? nextAttemptAtUtc, - Expression? lastErrorCode, - Expression? lastErrorMessage, - Expression? state, - Expression? lastError, - Expression? createdAtUtc, - Expression? updatedAtUtc, + Expression? collectionId, + Expression? hrefKey, + Expression? requestUri, + Expression? etag, + Expression? contentType, + Expression? dominantComponentType, + Expression? componentMask, + Expression? primaryUid, + Expression? rawIcsBody, + Expression? rawBodyHash, + Expression? semanticHash, + Expression? serverDeleted, + Expression? baselineGeneration, + Expression? firstSeenAtUtc, + Expression? lastFetchedAtUtc, + Expression? lastChangedAtUtc, + Expression? lastParseStatus, + Expression? lastParseErrorCode, + Expression? parserVersion, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (accountId != null) 'account_id': accountId, - if (provider != null) 'provider': provider, - if (entityType != null) 'entity_type': entityType, - if (operation != null) 'operation': operation, - if (operationType != null) 'operation_type': operationType, - if (taskListId != null) 'task_list_id': taskListId, - if (taskId != null) 'task_id': taskId, - if (calendarSourceId != null) 'calendar_source_id': calendarSourceId, - if (providerCalendarId != null) - 'provider_calendar_id': providerCalendarId, - if (eventId != null) 'event_id': eventId, - if (localTempId != null) 'local_temp_id': localTempId, - if (dependsOnOpId != null) 'depends_on_op_id': dependsOnOpId, - if (requestJson != null) 'request_json': requestJson, - if (baselineUpdatedUtc != null) - 'baseline_updated_utc': baselineUpdatedUtc, - if (baselineRawJson != null) 'baseline_raw_json': baselineRawJson, - if (attemptCount != null) 'attempt_count': attemptCount, - if (nextAttemptAtUtc != null) 'next_attempt_at_utc': nextAttemptAtUtc, - if (lastErrorCode != null) 'last_error_code': lastErrorCode, - if (lastErrorMessage != null) 'last_error_message': lastErrorMessage, - if (state != null) 'state': state, - if (lastError != null) 'last_error': lastError, - if (createdAtUtc != null) 'created_at_utc': createdAtUtc, - if (updatedAtUtc != null) 'updated_at_utc': updatedAtUtc, + if (collectionId != null) 'collection_id': collectionId, + if (hrefKey != null) 'href_key': hrefKey, + if (requestUri != null) 'request_uri': requestUri, + if (etag != null) 'etag': etag, + if (contentType != null) 'content_type': contentType, + if (dominantComponentType != null) + 'dominant_component_type': dominantComponentType, + if (componentMask != null) 'component_mask': componentMask, + if (primaryUid != null) 'primary_uid': primaryUid, + if (rawIcsBody != null) 'raw_ics_body': rawIcsBody, + if (rawBodyHash != null) 'raw_body_hash': rawBodyHash, + if (semanticHash != null) 'semantic_hash': semanticHash, + if (serverDeleted != null) 'server_deleted': serverDeleted, + if (baselineGeneration != null) 'baseline_generation': baselineGeneration, + if (firstSeenAtUtc != null) 'first_seen_at_utc': firstSeenAtUtc, + if (lastFetchedAtUtc != null) 'last_fetched_at_utc': lastFetchedAtUtc, + if (lastChangedAtUtc != null) 'last_changed_at_utc': lastChangedAtUtc, + if (lastParseStatus != null) 'last_parse_status': lastParseStatus, + if (lastParseErrorCode != null) + 'last_parse_error_code': lastParseErrorCode, + if (parserVersion != null) 'parser_version': parserVersion, if (rowid != null) 'rowid': rowid, }); } - PendingOpsCompanion copyWith({ + DavObjectsCompanion copyWith({ Value? id, Value? accountId, - Value? provider, - Value? entityType, - Value? operation, - Value? operationType, - Value? taskListId, - Value? taskId, - Value? calendarSourceId, - Value? providerCalendarId, - Value? eventId, - Value? localTempId, - Value? dependsOnOpId, - Value? requestJson, - Value? baselineUpdatedUtc, - Value? baselineRawJson, - Value? attemptCount, - Value? nextAttemptAtUtc, - Value? lastErrorCode, - Value? lastErrorMessage, - Value? state, - Value? lastError, - Value? createdAtUtc, - Value? updatedAtUtc, + Value? collectionId, + Value? hrefKey, + Value? requestUri, + Value? etag, + Value? contentType, + Value? dominantComponentType, + Value? componentMask, + Value? primaryUid, + Value? rawIcsBody, + Value? rawBodyHash, + Value? semanticHash, + Value? serverDeleted, + Value? baselineGeneration, + Value? firstSeenAtUtc, + Value? lastFetchedAtUtc, + Value? lastChangedAtUtc, + Value? lastParseStatus, + Value? lastParseErrorCode, + Value? parserVersion, Value? rowid, }) { - return PendingOpsCompanion( + return DavObjectsCompanion( id: id ?? this.id, accountId: accountId ?? this.accountId, - provider: provider ?? this.provider, - entityType: entityType ?? this.entityType, - operation: operation ?? this.operation, - operationType: operationType ?? this.operationType, - taskListId: taskListId ?? this.taskListId, - taskId: taskId ?? this.taskId, - calendarSourceId: calendarSourceId ?? this.calendarSourceId, - providerCalendarId: providerCalendarId ?? this.providerCalendarId, - eventId: eventId ?? this.eventId, - localTempId: localTempId ?? this.localTempId, - dependsOnOpId: dependsOnOpId ?? this.dependsOnOpId, - requestJson: requestJson ?? this.requestJson, - baselineUpdatedUtc: baselineUpdatedUtc ?? this.baselineUpdatedUtc, - baselineRawJson: baselineRawJson ?? this.baselineRawJson, - attemptCount: attemptCount ?? this.attemptCount, - nextAttemptAtUtc: nextAttemptAtUtc ?? this.nextAttemptAtUtc, - lastErrorCode: lastErrorCode ?? this.lastErrorCode, - lastErrorMessage: lastErrorMessage ?? this.lastErrorMessage, - state: state ?? this.state, - lastError: lastError ?? this.lastError, - createdAtUtc: createdAtUtc ?? this.createdAtUtc, - updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + collectionId: collectionId ?? this.collectionId, + hrefKey: hrefKey ?? this.hrefKey, + requestUri: requestUri ?? this.requestUri, + etag: etag ?? this.etag, + contentType: contentType ?? this.contentType, + dominantComponentType: + dominantComponentType ?? this.dominantComponentType, + componentMask: componentMask ?? this.componentMask, + primaryUid: primaryUid ?? this.primaryUid, + rawIcsBody: rawIcsBody ?? this.rawIcsBody, + rawBodyHash: rawBodyHash ?? this.rawBodyHash, + semanticHash: semanticHash ?? this.semanticHash, + serverDeleted: serverDeleted ?? this.serverDeleted, + baselineGeneration: baselineGeneration ?? this.baselineGeneration, + firstSeenAtUtc: firstSeenAtUtc ?? this.firstSeenAtUtc, + lastFetchedAtUtc: lastFetchedAtUtc ?? this.lastFetchedAtUtc, + lastChangedAtUtc: lastChangedAtUtc ?? this.lastChangedAtUtc, + lastParseStatus: lastParseStatus ?? this.lastParseStatus, + lastParseErrorCode: lastParseErrorCode ?? this.lastParseErrorCode, + parserVersion: parserVersion ?? this.parserVersion, rowid: rowid ?? this.rowid, ); } @@ -6020,71 +5232,64 @@ class PendingOpsCompanion extends UpdateCompanion { if (accountId.present) { map['account_id'] = Variable(accountId.value); } - if (provider.present) { - map['provider'] = Variable(provider.value); + if (collectionId.present) { + map['collection_id'] = Variable(collectionId.value); } - if (entityType.present) { - map['entity_type'] = Variable(entityType.value); + if (hrefKey.present) { + map['href_key'] = Variable(hrefKey.value); } - if (operation.present) { - map['operation'] = Variable(operation.value); + if (requestUri.present) { + map['request_uri'] = Variable(requestUri.value); } - if (operationType.present) { - map['operation_type'] = Variable(operationType.value); + if (etag.present) { + map['etag'] = Variable(etag.value); } - if (taskListId.present) { - map['task_list_id'] = Variable(taskListId.value); + if (contentType.present) { + map['content_type'] = Variable(contentType.value); } - if (taskId.present) { - map['task_id'] = Variable(taskId.value); + if (dominantComponentType.present) { + map['dominant_component_type'] = Variable( + dominantComponentType.value, + ); } - if (calendarSourceId.present) { - map['calendar_source_id'] = Variable(calendarSourceId.value); + if (componentMask.present) { + map['component_mask'] = Variable(componentMask.value); } - if (providerCalendarId.present) { - map['provider_calendar_id'] = Variable(providerCalendarId.value); + if (primaryUid.present) { + map['primary_uid'] = Variable(primaryUid.value); } - if (eventId.present) { - map['event_id'] = Variable(eventId.value); + if (rawIcsBody.present) { + map['raw_ics_body'] = Variable(rawIcsBody.value); } - if (localTempId.present) { - map['local_temp_id'] = Variable(localTempId.value); + if (rawBodyHash.present) { + map['raw_body_hash'] = Variable(rawBodyHash.value); } - if (dependsOnOpId.present) { - map['depends_on_op_id'] = Variable(dependsOnOpId.value); + if (semanticHash.present) { + map['semantic_hash'] = Variable(semanticHash.value); } - if (requestJson.present) { - map['request_json'] = Variable(requestJson.value); + if (serverDeleted.present) { + map['server_deleted'] = Variable(serverDeleted.value); } - if (baselineUpdatedUtc.present) { - map['baseline_updated_utc'] = Variable(baselineUpdatedUtc.value); + if (baselineGeneration.present) { + map['baseline_generation'] = Variable(baselineGeneration.value); } - if (baselineRawJson.present) { - map['baseline_raw_json'] = Variable(baselineRawJson.value); + if (firstSeenAtUtc.present) { + map['first_seen_at_utc'] = Variable(firstSeenAtUtc.value); } - if (attemptCount.present) { - map['attempt_count'] = Variable(attemptCount.value); + if (lastFetchedAtUtc.present) { + map['last_fetched_at_utc'] = Variable(lastFetchedAtUtc.value); } - if (nextAttemptAtUtc.present) { - map['next_attempt_at_utc'] = Variable(nextAttemptAtUtc.value); + if (lastChangedAtUtc.present) { + map['last_changed_at_utc'] = Variable(lastChangedAtUtc.value); } - if (lastErrorCode.present) { - map['last_error_code'] = Variable(lastErrorCode.value); + if (lastParseStatus.present) { + map['last_parse_status'] = Variable(lastParseStatus.value); } - if (lastErrorMessage.present) { - map['last_error_message'] = Variable(lastErrorMessage.value); + if (lastParseErrorCode.present) { + map['last_parse_error_code'] = Variable(lastParseErrorCode.value); } - if (state.present) { - map['state'] = Variable(state.value); - } - if (lastError.present) { - map['last_error'] = Variable(lastError.value); - } - if (createdAtUtc.present) { - map['created_at_utc'] = Variable(createdAtUtc.value); - } - if (updatedAtUtc.present) { - map['updated_at_utc'] = Variable(updatedAtUtc.value); + if (parserVersion.present) { + map['parser_version'] = Variable(parserVersion.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6094,42 +5299,40 @@ class PendingOpsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('PendingOpsCompanion(') + return (StringBuffer('DavObjectsCompanion(') ..write('id: $id, ') ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('entityType: $entityType, ') - ..write('operation: $operation, ') - ..write('operationType: $operationType, ') - ..write('taskListId: $taskListId, ') - ..write('taskId: $taskId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('eventId: $eventId, ') - ..write('localTempId: $localTempId, ') - ..write('dependsOnOpId: $dependsOnOpId, ') - ..write('requestJson: $requestJson, ') - ..write('baselineUpdatedUtc: $baselineUpdatedUtc, ') - ..write('baselineRawJson: $baselineRawJson, ') - ..write('attemptCount: $attemptCount, ') - ..write('nextAttemptAtUtc: $nextAttemptAtUtc, ') - ..write('lastErrorCode: $lastErrorCode, ') - ..write('lastErrorMessage: $lastErrorMessage, ') - ..write('state: $state, ') - ..write('lastError: $lastError, ') - ..write('createdAtUtc: $createdAtUtc, ') - ..write('updatedAtUtc: $updatedAtUtc, ') + ..write('collectionId: $collectionId, ') + ..write('hrefKey: $hrefKey, ') + ..write('requestUri: $requestUri, ') + ..write('etag: $etag, ') + ..write('contentType: $contentType, ') + ..write('dominantComponentType: $dominantComponentType, ') + ..write('componentMask: $componentMask, ') + ..write('primaryUid: $primaryUid, ') + ..write('rawIcsBody: $rawIcsBody, ') + ..write('rawBodyHash: $rawBodyHash, ') + ..write('semanticHash: $semanticHash, ') + ..write('serverDeleted: $serverDeleted, ') + ..write('baselineGeneration: $baselineGeneration, ') + ..write('firstSeenAtUtc: $firstSeenAtUtc, ') + ..write('lastFetchedAtUtc: $lastFetchedAtUtc, ') + ..write('lastChangedAtUtc: $lastChangedAtUtc, ') + ..write('lastParseStatus: $lastParseStatus, ') + ..write('lastParseErrorCode: $lastParseErrorCode, ') + ..write('parserVersion: $parserVersion, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $SyncRunsTable extends SyncRuns with TableInfo<$SyncRunsTable, SyncRun> { +class $DavObjectComponentsTable extends DavObjectComponents + with TableInfo<$DavObjectComponentsTable, DavObjectComponent> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $SyncRunsTable(this.attachedDatabase, [this._alias]); + $DavObjectComponentsTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -6139,152 +5342,127 @@ class $SyncRunsTable extends SyncRuns with TableInfo<$SyncRunsTable, SyncRun> { type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _accountIdMeta = const VerificationMeta( - 'accountId', + static const VerificationMeta _davObjectIdMeta = const VerificationMeta( + 'davObjectId', ); @override - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', + late final GeneratedColumn davObjectId = GeneratedColumn( + 'dav_object_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES accounts (id) ON DELETE CASCADE', + 'REFERENCES dav_objects (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', - ); - @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, + static const VerificationMeta _componentTypeMeta = const VerificationMeta( + 'componentType', ); - static const VerificationMeta _modeMeta = const VerificationMeta('mode'); @override - late final GeneratedColumn mode = GeneratedColumn( - 'mode', + late final GeneratedColumn componentType = GeneratedColumn( + 'component_type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _startedAtUtcMeta = const VerificationMeta( - 'startedAtUtc', - ); + static const VerificationMeta _uidMeta = const VerificationMeta('uid'); @override - late final GeneratedColumn startedAtUtc = GeneratedColumn( - 'started_at_utc', + late final GeneratedColumn uid = GeneratedColumn( + 'uid', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _finishedAtUtcMeta = const VerificationMeta( - 'finishedAtUtc', + static const VerificationMeta _recurrenceIdKeyMeta = const VerificationMeta( + 'recurrenceIdKey', ); @override - late final GeneratedColumn finishedAtUtc = GeneratedColumn( - 'finished_at_utc', + late final GeneratedColumn recurrenceIdKey = GeneratedColumn( + 'recurrence_id_key', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _statusMeta = const VerificationMeta('status'); - @override - late final GeneratedColumn status = GeneratedColumn( - 'status', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _taskListsSeenMeta = const VerificationMeta( - 'taskListsSeen', + static const VerificationMeta _sequenceMeta = const VerificationMeta( + 'sequence', ); @override - late final GeneratedColumn taskListsSeen = GeneratedColumn( - 'task_lists_seen', + late final GeneratedColumn sequence = GeneratedColumn( + 'sequence', aliasedName, - false, + true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(0), ); - static const VerificationMeta _tasksSeenMeta = const VerificationMeta( - 'tasksSeen', + static const VerificationMeta _dtstampUtcMeta = const VerificationMeta( + 'dtstampUtc', ); @override - late final GeneratedColumn tasksSeen = GeneratedColumn( - 'tasks_seen', + late final GeneratedColumn dtstampUtc = GeneratedColumn( + 'dtstamp_utc', aliasedName, - false, - type: DriftSqlType.int, + true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant(0), ); - static const VerificationMeta _pendingOpsAppliedMeta = const VerificationMeta( - 'pendingOpsApplied', + static const VerificationMeta _lastModifiedUtcMeta = const VerificationMeta( + 'lastModifiedUtc', ); @override - late final GeneratedColumn pendingOpsApplied = GeneratedColumn( - 'pending_ops_applied', + late final GeneratedColumn lastModifiedUtc = GeneratedColumn( + 'last_modified_utc', aliasedName, - false, - type: DriftSqlType.int, + true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant(0), ); - static const VerificationMeta _errorCodeMeta = const VerificationMeta( - 'errorCode', + static const VerificationMeta _semanticHashMeta = const VerificationMeta( + 'semanticHash', ); @override - late final GeneratedColumn errorCode = GeneratedColumn( - 'error_code', + late final GeneratedColumn semanticHash = GeneratedColumn( + 'semantic_hash', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _errorMessageMeta = const VerificationMeta( - 'errorMessage', + requiredDuringInsert: true, ); + static const VerificationMeta _parserProfileVersionMeta = + const VerificationMeta('parserProfileVersion'); @override - late final GeneratedColumn errorMessage = GeneratedColumn( - 'error_message', + late final GeneratedColumn parserProfileVersion = GeneratedColumn( + 'parser_profile_version', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.int, requiredDuringInsert: false, + defaultValue: const Constant(1), ); @override List get $columns => [ id, - accountId, - provider, - mode, - startedAtUtc, - finishedAtUtc, - status, - taskListsSeen, - tasksSeen, - pendingOpsApplied, - errorCode, - errorMessage, + davObjectId, + componentType, + uid, + recurrenceIdKey, + sequence, + dtstampUtc, + lastModifiedUtc, + semanticHash, + parserProfileVersion, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'sync_runs'; + static const String $name = 'dav_object_components'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); @@ -6294,92 +5472,83 @@ class $SyncRunsTable extends SyncRuns with TableInfo<$SyncRunsTable, SyncRun> { } else if (isInserting) { context.missing(_idMeta); } - if (data.containsKey('account_id')) { + if (data.containsKey('dav_object_id')) { context.handle( - _accountIdMeta, - accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + _davObjectIdMeta, + davObjectId.isAcceptableOrUnknown( + data['dav_object_id']!, + _davObjectIdMeta, + ), ); } else if (isInserting) { - context.missing(_accountIdMeta); - } - if (data.containsKey('provider')) { - context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), - ); + context.missing(_davObjectIdMeta); } - if (data.containsKey('mode')) { + if (data.containsKey('component_type')) { context.handle( - _modeMeta, - mode.isAcceptableOrUnknown(data['mode']!, _modeMeta), + _componentTypeMeta, + componentType.isAcceptableOrUnknown( + data['component_type']!, + _componentTypeMeta, + ), ); } else if (isInserting) { - context.missing(_modeMeta); + context.missing(_componentTypeMeta); } - if (data.containsKey('started_at_utc')) { + if (data.containsKey('uid')) { context.handle( - _startedAtUtcMeta, - startedAtUtc.isAcceptableOrUnknown( - data['started_at_utc']!, - _startedAtUtcMeta, - ), + _uidMeta, + uid.isAcceptableOrUnknown(data['uid']!, _uidMeta), ); } else if (isInserting) { - context.missing(_startedAtUtcMeta); + context.missing(_uidMeta); } - if (data.containsKey('finished_at_utc')) { + if (data.containsKey('recurrence_id_key')) { context.handle( - _finishedAtUtcMeta, - finishedAtUtc.isAcceptableOrUnknown( - data['finished_at_utc']!, - _finishedAtUtcMeta, + _recurrenceIdKeyMeta, + recurrenceIdKey.isAcceptableOrUnknown( + data['recurrence_id_key']!, + _recurrenceIdKeyMeta, ), ); } - if (data.containsKey('status')) { - context.handle( - _statusMeta, - status.isAcceptableOrUnknown(data['status']!, _statusMeta), - ); - } else if (isInserting) { - context.missing(_statusMeta); - } - if (data.containsKey('task_lists_seen')) { + if (data.containsKey('sequence')) { context.handle( - _taskListsSeenMeta, - taskListsSeen.isAcceptableOrUnknown( - data['task_lists_seen']!, - _taskListsSeenMeta, - ), + _sequenceMeta, + sequence.isAcceptableOrUnknown(data['sequence']!, _sequenceMeta), ); } - if (data.containsKey('tasks_seen')) { + if (data.containsKey('dtstamp_utc')) { context.handle( - _tasksSeenMeta, - tasksSeen.isAcceptableOrUnknown(data['tasks_seen']!, _tasksSeenMeta), + _dtstampUtcMeta, + dtstampUtc.isAcceptableOrUnknown(data['dtstamp_utc']!, _dtstampUtcMeta), ); } - if (data.containsKey('pending_ops_applied')) { + if (data.containsKey('last_modified_utc')) { context.handle( - _pendingOpsAppliedMeta, - pendingOpsApplied.isAcceptableOrUnknown( - data['pending_ops_applied']!, - _pendingOpsAppliedMeta, + _lastModifiedUtcMeta, + lastModifiedUtc.isAcceptableOrUnknown( + data['last_modified_utc']!, + _lastModifiedUtcMeta, ), ); } - if (data.containsKey('error_code')) { + if (data.containsKey('semantic_hash')) { context.handle( - _errorCodeMeta, - errorCode.isAcceptableOrUnknown(data['error_code']!, _errorCodeMeta), + _semanticHashMeta, + semanticHash.isAcceptableOrUnknown( + data['semantic_hash']!, + _semanticHashMeta, + ), ); + } else if (isInserting) { + context.missing(_semanticHashMeta); } - if (data.containsKey('error_message')) { + if (data.containsKey('parser_profile_version')) { context.handle( - _errorMessageMeta, - errorMessage.isAcceptableOrUnknown( - data['error_message']!, - _errorMessageMeta, + _parserProfileVersionMeta, + parserProfileVersion.isAcceptableOrUnknown( + data['parser_profile_version']!, + _parserProfileVersionMeta, ), ); } @@ -6389,162 +5558,147 @@ class $SyncRunsTable extends SyncRuns with TableInfo<$SyncRunsTable, SyncRun> { @override Set get $primaryKey => {id}; @override - SyncRun map(Map data, {String? tablePrefix}) { + DavObjectComponent map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return SyncRun( + return DavObjectComponent( id: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}id'], )!, - accountId: attachedDatabase.typeMapping.read( + davObjectId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}account_id'], + data['${effectivePrefix}dav_object_id'], )!, - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - ), - mode: attachedDatabase.typeMapping.read( + componentType: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}mode'], + data['${effectivePrefix}component_type'], )!, - startedAtUtc: attachedDatabase.typeMapping.read( + uid: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}started_at_utc'], + data['${effectivePrefix}uid'], )!, - finishedAtUtc: attachedDatabase.typeMapping.read( + recurrenceIdKey: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}finished_at_utc'], + data['${effectivePrefix}recurrence_id_key'], ), - status: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}status'], - )!, - taskListsSeen: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}task_lists_seen'], - )!, - tasksSeen: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}tasks_seen'], - )!, - pendingOpsApplied: attachedDatabase.typeMapping.read( + sequence: attachedDatabase.typeMapping.read( DriftSqlType.int, - data['${effectivePrefix}pending_ops_applied'], - )!, - errorCode: attachedDatabase.typeMapping.read( + data['${effectivePrefix}sequence'], + ), + dtstampUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}error_code'], + data['${effectivePrefix}dtstamp_utc'], ), - errorMessage: attachedDatabase.typeMapping.read( + lastModifiedUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}error_message'], + data['${effectivePrefix}last_modified_utc'], ), + semanticHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}semantic_hash'], + )!, + parserProfileVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}parser_profile_version'], + )!, ); } @override - $SyncRunsTable createAlias(String alias) { - return $SyncRunsTable(attachedDatabase, alias); + $DavObjectComponentsTable createAlias(String alias) { + return $DavObjectComponentsTable(attachedDatabase, alias); } } -class SyncRun extends DataClass implements Insertable { +class DavObjectComponent extends DataClass + implements Insertable { final String id; - final String accountId; - final String? provider; - final String mode; - final String startedAtUtc; - final String? finishedAtUtc; - final String status; - final int taskListsSeen; - final int tasksSeen; - final int pendingOpsApplied; - final String? errorCode; - final String? errorMessage; - const SyncRun({ + final String davObjectId; + final String componentType; + final String uid; + final String? recurrenceIdKey; + final int? sequence; + final String? dtstampUtc; + final String? lastModifiedUtc; + final String semanticHash; + final int parserProfileVersion; + const DavObjectComponent({ required this.id, - required this.accountId, - this.provider, - required this.mode, - required this.startedAtUtc, - this.finishedAtUtc, - required this.status, - required this.taskListsSeen, - required this.tasksSeen, - required this.pendingOpsApplied, - this.errorCode, - this.errorMessage, + required this.davObjectId, + required this.componentType, + required this.uid, + this.recurrenceIdKey, + this.sequence, + this.dtstampUtc, + this.lastModifiedUtc, + required this.semanticHash, + required this.parserProfileVersion, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - if (!nullToAbsent || provider != null) { - map['provider'] = Variable(provider); + map['dav_object_id'] = Variable(davObjectId); + map['component_type'] = Variable(componentType); + map['uid'] = Variable(uid); + if (!nullToAbsent || recurrenceIdKey != null) { + map['recurrence_id_key'] = Variable(recurrenceIdKey); } - map['mode'] = Variable(mode); - map['started_at_utc'] = Variable(startedAtUtc); - if (!nullToAbsent || finishedAtUtc != null) { - map['finished_at_utc'] = Variable(finishedAtUtc); + if (!nullToAbsent || sequence != null) { + map['sequence'] = Variable(sequence); } - map['status'] = Variable(status); - map['task_lists_seen'] = Variable(taskListsSeen); - map['tasks_seen'] = Variable(tasksSeen); - map['pending_ops_applied'] = Variable(pendingOpsApplied); - if (!nullToAbsent || errorCode != null) { - map['error_code'] = Variable(errorCode); + if (!nullToAbsent || dtstampUtc != null) { + map['dtstamp_utc'] = Variable(dtstampUtc); } - if (!nullToAbsent || errorMessage != null) { - map['error_message'] = Variable(errorMessage); + if (!nullToAbsent || lastModifiedUtc != null) { + map['last_modified_utc'] = Variable(lastModifiedUtc); } + map['semantic_hash'] = Variable(semanticHash); + map['parser_profile_version'] = Variable(parserProfileVersion); return map; } - SyncRunsCompanion toCompanion(bool nullToAbsent) { - return SyncRunsCompanion( + DavObjectComponentsCompanion toCompanion(bool nullToAbsent) { + return DavObjectComponentsCompanion( id: Value(id), - accountId: Value(accountId), - provider: provider == null && nullToAbsent + davObjectId: Value(davObjectId), + componentType: Value(componentType), + uid: Value(uid), + recurrenceIdKey: recurrenceIdKey == null && nullToAbsent ? const Value.absent() - : Value(provider), - mode: Value(mode), - startedAtUtc: Value(startedAtUtc), - finishedAtUtc: finishedAtUtc == null && nullToAbsent + : Value(recurrenceIdKey), + sequence: sequence == null && nullToAbsent ? const Value.absent() - : Value(finishedAtUtc), - status: Value(status), - taskListsSeen: Value(taskListsSeen), - tasksSeen: Value(tasksSeen), - pendingOpsApplied: Value(pendingOpsApplied), - errorCode: errorCode == null && nullToAbsent + : Value(sequence), + dtstampUtc: dtstampUtc == null && nullToAbsent ? const Value.absent() - : Value(errorCode), - errorMessage: errorMessage == null && nullToAbsent + : Value(dtstampUtc), + lastModifiedUtc: lastModifiedUtc == null && nullToAbsent ? const Value.absent() - : Value(errorMessage), + : Value(lastModifiedUtc), + semanticHash: Value(semanticHash), + parserProfileVersion: Value(parserProfileVersion), ); } - factory SyncRun.fromJson( + factory DavObjectComponent.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return SyncRun( + return DavObjectComponent( id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - provider: serializer.fromJson(json['provider']), - mode: serializer.fromJson(json['mode']), - startedAtUtc: serializer.fromJson(json['startedAtUtc']), - finishedAtUtc: serializer.fromJson(json['finishedAtUtc']), - status: serializer.fromJson(json['status']), - taskListsSeen: serializer.fromJson(json['taskListsSeen']), - tasksSeen: serializer.fromJson(json['tasksSeen']), - pendingOpsApplied: serializer.fromJson(json['pendingOpsApplied']), - errorCode: serializer.fromJson(json['errorCode']), - errorMessage: serializer.fromJson(json['errorMessage']), + davObjectId: serializer.fromJson(json['davObjectId']), + componentType: serializer.fromJson(json['componentType']), + uid: serializer.fromJson(json['uid']), + recurrenceIdKey: serializer.fromJson(json['recurrenceIdKey']), + sequence: serializer.fromJson(json['sequence']), + dtstampUtc: serializer.fromJson(json['dtstampUtc']), + lastModifiedUtc: serializer.fromJson(json['lastModifiedUtc']), + semanticHash: serializer.fromJson(json['semanticHash']), + parserProfileVersion: serializer.fromJson( + json['parserProfileVersion'], + ), ); } @override @@ -6552,91 +5706,87 @@ class SyncRun extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'provider': serializer.toJson(provider), - 'mode': serializer.toJson(mode), - 'startedAtUtc': serializer.toJson(startedAtUtc), - 'finishedAtUtc': serializer.toJson(finishedAtUtc), - 'status': serializer.toJson(status), - 'taskListsSeen': serializer.toJson(taskListsSeen), - 'tasksSeen': serializer.toJson(tasksSeen), - 'pendingOpsApplied': serializer.toJson(pendingOpsApplied), - 'errorCode': serializer.toJson(errorCode), - 'errorMessage': serializer.toJson(errorMessage), + 'davObjectId': serializer.toJson(davObjectId), + 'componentType': serializer.toJson(componentType), + 'uid': serializer.toJson(uid), + 'recurrenceIdKey': serializer.toJson(recurrenceIdKey), + 'sequence': serializer.toJson(sequence), + 'dtstampUtc': serializer.toJson(dtstampUtc), + 'lastModifiedUtc': serializer.toJson(lastModifiedUtc), + 'semanticHash': serializer.toJson(semanticHash), + 'parserProfileVersion': serializer.toJson(parserProfileVersion), }; } - SyncRun copyWith({ + DavObjectComponent copyWith({ String? id, - String? accountId, - Value provider = const Value.absent(), - String? mode, - String? startedAtUtc, - Value finishedAtUtc = const Value.absent(), - String? status, - int? taskListsSeen, - int? tasksSeen, - int? pendingOpsApplied, - Value errorCode = const Value.absent(), - Value errorMessage = const Value.absent(), - }) => SyncRun( + String? davObjectId, + String? componentType, + String? uid, + Value recurrenceIdKey = const Value.absent(), + Value sequence = const Value.absent(), + Value dtstampUtc = const Value.absent(), + Value lastModifiedUtc = const Value.absent(), + String? semanticHash, + int? parserProfileVersion, + }) => DavObjectComponent( id: id ?? this.id, - accountId: accountId ?? this.accountId, - provider: provider.present ? provider.value : this.provider, - mode: mode ?? this.mode, - startedAtUtc: startedAtUtc ?? this.startedAtUtc, - finishedAtUtc: finishedAtUtc.present - ? finishedAtUtc.value - : this.finishedAtUtc, - status: status ?? this.status, - taskListsSeen: taskListsSeen ?? this.taskListsSeen, - tasksSeen: tasksSeen ?? this.tasksSeen, - pendingOpsApplied: pendingOpsApplied ?? this.pendingOpsApplied, - errorCode: errorCode.present ? errorCode.value : this.errorCode, - errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage, - ); - SyncRun copyWithCompanion(SyncRunsCompanion data) { - return SyncRun( + davObjectId: davObjectId ?? this.davObjectId, + componentType: componentType ?? this.componentType, + uid: uid ?? this.uid, + recurrenceIdKey: recurrenceIdKey.present + ? recurrenceIdKey.value + : this.recurrenceIdKey, + sequence: sequence.present ? sequence.value : this.sequence, + dtstampUtc: dtstampUtc.present ? dtstampUtc.value : this.dtstampUtc, + lastModifiedUtc: lastModifiedUtc.present + ? lastModifiedUtc.value + : this.lastModifiedUtc, + semanticHash: semanticHash ?? this.semanticHash, + parserProfileVersion: parserProfileVersion ?? this.parserProfileVersion, + ); + DavObjectComponent copyWithCompanion(DavObjectComponentsCompanion data) { + return DavObjectComponent( id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - provider: data.provider.present ? data.provider.value : this.provider, - mode: data.mode.present ? data.mode.value : this.mode, - startedAtUtc: data.startedAtUtc.present - ? data.startedAtUtc.value - : this.startedAtUtc, - finishedAtUtc: data.finishedAtUtc.present - ? data.finishedAtUtc.value - : this.finishedAtUtc, - status: data.status.present ? data.status.value : this.status, - taskListsSeen: data.taskListsSeen.present - ? data.taskListsSeen.value - : this.taskListsSeen, - tasksSeen: data.tasksSeen.present ? data.tasksSeen.value : this.tasksSeen, - pendingOpsApplied: data.pendingOpsApplied.present - ? data.pendingOpsApplied.value - : this.pendingOpsApplied, - errorCode: data.errorCode.present ? data.errorCode.value : this.errorCode, - errorMessage: data.errorMessage.present - ? data.errorMessage.value - : this.errorMessage, + davObjectId: data.davObjectId.present + ? data.davObjectId.value + : this.davObjectId, + componentType: data.componentType.present + ? data.componentType.value + : this.componentType, + uid: data.uid.present ? data.uid.value : this.uid, + recurrenceIdKey: data.recurrenceIdKey.present + ? data.recurrenceIdKey.value + : this.recurrenceIdKey, + sequence: data.sequence.present ? data.sequence.value : this.sequence, + dtstampUtc: data.dtstampUtc.present + ? data.dtstampUtc.value + : this.dtstampUtc, + lastModifiedUtc: data.lastModifiedUtc.present + ? data.lastModifiedUtc.value + : this.lastModifiedUtc, + semanticHash: data.semanticHash.present + ? data.semanticHash.value + : this.semanticHash, + parserProfileVersion: data.parserProfileVersion.present + ? data.parserProfileVersion.value + : this.parserProfileVersion, ); } @override String toString() { - return (StringBuffer('SyncRun(') + return (StringBuffer('DavObjectComponent(') ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('mode: $mode, ') - ..write('startedAtUtc: $startedAtUtc, ') - ..write('finishedAtUtc: $finishedAtUtc, ') - ..write('status: $status, ') - ..write('taskListsSeen: $taskListsSeen, ') - ..write('tasksSeen: $tasksSeen, ') - ..write('pendingOpsApplied: $pendingOpsApplied, ') - ..write('errorCode: $errorCode, ') - ..write('errorMessage: $errorMessage') + ..write('davObjectId: $davObjectId, ') + ..write('componentType: $componentType, ') + ..write('uid: $uid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('sequence: $sequence, ') + ..write('dtstampUtc: $dtstampUtc, ') + ..write('lastModifiedUtc: $lastModifiedUtc, ') + ..write('semanticHash: $semanticHash, ') + ..write('parserProfileVersion: $parserProfileVersion') ..write(')')) .toString(); } @@ -6644,144 +5794,127 @@ class SyncRun extends DataClass implements Insertable { @override int get hashCode => Object.hash( id, - accountId, - provider, - mode, - startedAtUtc, - finishedAtUtc, - status, - taskListsSeen, - tasksSeen, - pendingOpsApplied, - errorCode, - errorMessage, + davObjectId, + componentType, + uid, + recurrenceIdKey, + sequence, + dtstampUtc, + lastModifiedUtc, + semanticHash, + parserProfileVersion, ); @override bool operator ==(Object other) => identical(this, other) || - (other is SyncRun && + (other is DavObjectComponent && other.id == this.id && - other.accountId == this.accountId && - other.provider == this.provider && - other.mode == this.mode && - other.startedAtUtc == this.startedAtUtc && - other.finishedAtUtc == this.finishedAtUtc && - other.status == this.status && - other.taskListsSeen == this.taskListsSeen && - other.tasksSeen == this.tasksSeen && - other.pendingOpsApplied == this.pendingOpsApplied && - other.errorCode == this.errorCode && - other.errorMessage == this.errorMessage); + other.davObjectId == this.davObjectId && + other.componentType == this.componentType && + other.uid == this.uid && + other.recurrenceIdKey == this.recurrenceIdKey && + other.sequence == this.sequence && + other.dtstampUtc == this.dtstampUtc && + other.lastModifiedUtc == this.lastModifiedUtc && + other.semanticHash == this.semanticHash && + other.parserProfileVersion == this.parserProfileVersion); } -class SyncRunsCompanion extends UpdateCompanion { +class DavObjectComponentsCompanion extends UpdateCompanion { final Value id; - final Value accountId; - final Value provider; - final Value mode; - final Value startedAtUtc; - final Value finishedAtUtc; - final Value status; - final Value taskListsSeen; - final Value tasksSeen; - final Value pendingOpsApplied; - final Value errorCode; - final Value errorMessage; + final Value davObjectId; + final Value componentType; + final Value uid; + final Value recurrenceIdKey; + final Value sequence; + final Value dtstampUtc; + final Value lastModifiedUtc; + final Value semanticHash; + final Value parserProfileVersion; final Value rowid; - const SyncRunsCompanion({ + const DavObjectComponentsCompanion({ this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.provider = const Value.absent(), - this.mode = const Value.absent(), - this.startedAtUtc = const Value.absent(), - this.finishedAtUtc = const Value.absent(), - this.status = const Value.absent(), - this.taskListsSeen = const Value.absent(), - this.tasksSeen = const Value.absent(), - this.pendingOpsApplied = const Value.absent(), - this.errorCode = const Value.absent(), - this.errorMessage = const Value.absent(), + this.davObjectId = const Value.absent(), + this.componentType = const Value.absent(), + this.uid = const Value.absent(), + this.recurrenceIdKey = const Value.absent(), + this.sequence = const Value.absent(), + this.dtstampUtc = const Value.absent(), + this.lastModifiedUtc = const Value.absent(), + this.semanticHash = const Value.absent(), + this.parserProfileVersion = const Value.absent(), this.rowid = const Value.absent(), }); - SyncRunsCompanion.insert({ + DavObjectComponentsCompanion.insert({ required String id, - required String accountId, - this.provider = const Value.absent(), - required String mode, - required String startedAtUtc, - this.finishedAtUtc = const Value.absent(), - required String status, - this.taskListsSeen = const Value.absent(), - this.tasksSeen = const Value.absent(), - this.pendingOpsApplied = const Value.absent(), - this.errorCode = const Value.absent(), - this.errorMessage = const Value.absent(), + required String davObjectId, + required String componentType, + required String uid, + this.recurrenceIdKey = const Value.absent(), + this.sequence = const Value.absent(), + this.dtstampUtc = const Value.absent(), + this.lastModifiedUtc = const Value.absent(), + required String semanticHash, + this.parserProfileVersion = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), - accountId = Value(accountId), - mode = Value(mode), - startedAtUtc = Value(startedAtUtc), - status = Value(status); - static Insertable custom({ + davObjectId = Value(davObjectId), + componentType = Value(componentType), + uid = Value(uid), + semanticHash = Value(semanticHash); + static Insertable custom({ Expression? id, - Expression? accountId, - Expression? provider, - Expression? mode, - Expression? startedAtUtc, - Expression? finishedAtUtc, - Expression? status, - Expression? taskListsSeen, - Expression? tasksSeen, - Expression? pendingOpsApplied, - Expression? errorCode, - Expression? errorMessage, + Expression? davObjectId, + Expression? componentType, + Expression? uid, + Expression? recurrenceIdKey, + Expression? sequence, + Expression? dtstampUtc, + Expression? lastModifiedUtc, + Expression? semanticHash, + Expression? parserProfileVersion, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (provider != null) 'provider': provider, - if (mode != null) 'mode': mode, - if (startedAtUtc != null) 'started_at_utc': startedAtUtc, - if (finishedAtUtc != null) 'finished_at_utc': finishedAtUtc, - if (status != null) 'status': status, - if (taskListsSeen != null) 'task_lists_seen': taskListsSeen, - if (tasksSeen != null) 'tasks_seen': tasksSeen, - if (pendingOpsApplied != null) 'pending_ops_applied': pendingOpsApplied, - if (errorCode != null) 'error_code': errorCode, - if (errorMessage != null) 'error_message': errorMessage, + if (davObjectId != null) 'dav_object_id': davObjectId, + if (componentType != null) 'component_type': componentType, + if (uid != null) 'uid': uid, + if (recurrenceIdKey != null) 'recurrence_id_key': recurrenceIdKey, + if (sequence != null) 'sequence': sequence, + if (dtstampUtc != null) 'dtstamp_utc': dtstampUtc, + if (lastModifiedUtc != null) 'last_modified_utc': lastModifiedUtc, + if (semanticHash != null) 'semantic_hash': semanticHash, + if (parserProfileVersion != null) + 'parser_profile_version': parserProfileVersion, if (rowid != null) 'rowid': rowid, }); } - SyncRunsCompanion copyWith({ + DavObjectComponentsCompanion copyWith({ Value? id, - Value? accountId, - Value? provider, - Value? mode, - Value? startedAtUtc, - Value? finishedAtUtc, - Value? status, - Value? taskListsSeen, - Value? tasksSeen, - Value? pendingOpsApplied, - Value? errorCode, - Value? errorMessage, + Value? davObjectId, + Value? componentType, + Value? uid, + Value? recurrenceIdKey, + Value? sequence, + Value? dtstampUtc, + Value? lastModifiedUtc, + Value? semanticHash, + Value? parserProfileVersion, Value? rowid, }) { - return SyncRunsCompanion( + return DavObjectComponentsCompanion( id: id ?? this.id, - accountId: accountId ?? this.accountId, - provider: provider ?? this.provider, - mode: mode ?? this.mode, - startedAtUtc: startedAtUtc ?? this.startedAtUtc, - finishedAtUtc: finishedAtUtc ?? this.finishedAtUtc, - status: status ?? this.status, - taskListsSeen: taskListsSeen ?? this.taskListsSeen, - tasksSeen: tasksSeen ?? this.tasksSeen, - pendingOpsApplied: pendingOpsApplied ?? this.pendingOpsApplied, - errorCode: errorCode ?? this.errorCode, - errorMessage: errorMessage ?? this.errorMessage, + davObjectId: davObjectId ?? this.davObjectId, + componentType: componentType ?? this.componentType, + uid: uid ?? this.uid, + recurrenceIdKey: recurrenceIdKey ?? this.recurrenceIdKey, + sequence: sequence ?? this.sequence, + dtstampUtc: dtstampUtc ?? this.dtstampUtc, + lastModifiedUtc: lastModifiedUtc ?? this.lastModifiedUtc, + semanticHash: semanticHash ?? this.semanticHash, + parserProfileVersion: parserProfileVersion ?? this.parserProfileVersion, rowid: rowid ?? this.rowid, ); } @@ -6792,38 +5925,32 @@ class SyncRunsCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (mode.present) { - map['mode'] = Variable(mode.value); + if (davObjectId.present) { + map['dav_object_id'] = Variable(davObjectId.value); } - if (startedAtUtc.present) { - map['started_at_utc'] = Variable(startedAtUtc.value); + if (componentType.present) { + map['component_type'] = Variable(componentType.value); } - if (finishedAtUtc.present) { - map['finished_at_utc'] = Variable(finishedAtUtc.value); + if (uid.present) { + map['uid'] = Variable(uid.value); } - if (status.present) { - map['status'] = Variable(status.value); + if (recurrenceIdKey.present) { + map['recurrence_id_key'] = Variable(recurrenceIdKey.value); } - if (taskListsSeen.present) { - map['task_lists_seen'] = Variable(taskListsSeen.value); + if (sequence.present) { + map['sequence'] = Variable(sequence.value); } - if (tasksSeen.present) { - map['tasks_seen'] = Variable(tasksSeen.value); + if (dtstampUtc.present) { + map['dtstamp_utc'] = Variable(dtstampUtc.value); } - if (pendingOpsApplied.present) { - map['pending_ops_applied'] = Variable(pendingOpsApplied.value); + if (lastModifiedUtc.present) { + map['last_modified_utc'] = Variable(lastModifiedUtc.value); } - if (errorCode.present) { - map['error_code'] = Variable(errorCode.value); + if (semanticHash.present) { + map['semantic_hash'] = Variable(semanticHash.value); } - if (errorMessage.present) { - map['error_message'] = Variable(errorMessage.value); + if (parserProfileVersion.present) { + map['parser_profile_version'] = Variable(parserProfileVersion.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6833,31 +5960,29 @@ class SyncRunsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('SyncRunsCompanion(') + return (StringBuffer('DavObjectComponentsCompanion(') ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('mode: $mode, ') - ..write('startedAtUtc: $startedAtUtc, ') - ..write('finishedAtUtc: $finishedAtUtc, ') - ..write('status: $status, ') - ..write('taskListsSeen: $taskListsSeen, ') - ..write('tasksSeen: $tasksSeen, ') - ..write('pendingOpsApplied: $pendingOpsApplied, ') - ..write('errorCode: $errorCode, ') - ..write('errorMessage: $errorMessage, ') + ..write('davObjectId: $davObjectId, ') + ..write('componentType: $componentType, ') + ..write('uid: $uid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('sequence: $sequence, ') + ..write('dtstampUtc: $dtstampUtc, ') + ..write('lastModifiedUtc: $lastModifiedUtc, ') + ..write('semanticHash: $semanticHash, ') + ..write('parserProfileVersion: $parserProfileVersion, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $CalendarSourcesTable extends CalendarSources - with TableInfo<$CalendarSourcesTable, CalendarSource> { +class $DavConflictSnapshotsTable extends DavConflictSnapshots + with TableInfo<$DavConflictSnapshotsTable, DavConflictSnapshot> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $CalendarSourcesTable(this.attachedDatabase, [this._alias]); + $DavConflictSnapshotsTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( @@ -6881,241 +6006,157 @@ class $CalendarSourcesTable extends CalendarSources 'REFERENCES accounts (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', - ); - @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _providerCalendarIdMeta = - const VerificationMeta('providerCalendarId'); - @override - late final GeneratedColumn providerCalendarId = - GeneratedColumn( - 'provider_calendar_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _summaryMeta = const VerificationMeta( - 'summary', - ); - @override - late final GeneratedColumn summary = GeneratedColumn( - 'summary', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _descriptionMeta = const VerificationMeta( - 'description', + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', ); @override - late final GeneratedColumn description = GeneratedColumn( - 'description', + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - ); - static const VerificationMeta _primaryCalendarMeta = const VerificationMeta( - 'primaryCalendar', - ); - @override - late final GeneratedColumn primaryCalendar = GeneratedColumn( - 'primary_calendar', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("primary_calendar" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _selectedMeta = const VerificationMeta( - 'selected', - ); - @override - late final GeneratedColumn selected = GeneratedColumn( - 'selected', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("selected" IN (0, 1))', - ), - defaultValue: const Constant(true), - ); - static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); - @override - late final GeneratedColumn hidden = GeneratedColumn( - 'hidden', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("hidden" IN (0, 1))', + 'REFERENCES dav_collections (id) ON DELETE SET NULL', ), - defaultValue: const Constant(false), ); - static const VerificationMeta _readOnlyMeta = const VerificationMeta( - 'readOnly', + static const VerificationMeta _davObjectIdMeta = const VerificationMeta( + 'davObjectId', ); @override - late final GeneratedColumn readOnly = GeneratedColumn( - 'read_only', + late final GeneratedColumn davObjectId = GeneratedColumn( + 'dav_object_id', aliasedName, - false, - type: DriftSqlType.bool, + true, + type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("read_only" IN (0, 1))', + 'REFERENCES dav_objects (id) ON DELETE SET NULL', ), - defaultValue: const Constant(false), ); - static const VerificationMeta _backgroundColorMeta = const VerificationMeta( - 'backgroundColor', + static const VerificationMeta _baselineEtagMeta = const VerificationMeta( + 'baselineEtag', ); @override - late final GeneratedColumn backgroundColor = GeneratedColumn( - 'background_color', + late final GeneratedColumn baselineEtag = GeneratedColumn( + 'baseline_etag', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _foregroundColorMeta = const VerificationMeta( - 'foregroundColor', + static const VerificationMeta _baselineRawIcsMeta = const VerificationMeta( + 'baselineRawIcs', ); @override - late final GeneratedColumn foregroundColor = GeneratedColumn( - 'foreground_color', + late final GeneratedColumn baselineRawIcs = GeneratedColumn( + 'baseline_raw_ics', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _colorIdMeta = const VerificationMeta( - 'colorId', + static const VerificationMeta _localCandidateRawIcsMeta = + const VerificationMeta('localCandidateRawIcs'); + @override + late final GeneratedColumn localCandidateRawIcs = + GeneratedColumn( + 'local_candidate_raw_ics', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _remoteEtagMeta = const VerificationMeta( + 'remoteEtag', ); @override - late final GeneratedColumn colorId = GeneratedColumn( - 'color_id', + late final GeneratedColumn remoteEtag = GeneratedColumn( + 'remote_etag', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _timeZoneMeta = const VerificationMeta( - 'timeZone', + static const VerificationMeta _remoteRawIcsMeta = const VerificationMeta( + 'remoteRawIcs', ); @override - late final GeneratedColumn timeZone = GeneratedColumn( - 'time_zone', + late final GeneratedColumn remoteRawIcs = GeneratedColumn( + 'remote_raw_ics', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _accessRoleMeta = const VerificationMeta( - 'accessRole', + static const VerificationMeta _conflictCodeMeta = const VerificationMeta( + 'conflictCode', ); @override - late final GeneratedColumn accessRole = GeneratedColumn( - 'access_role', + late final GeneratedColumn conflictCode = GeneratedColumn( + 'conflict_code', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _isDeletedMeta = const VerificationMeta( - 'isDeleted', + static const VerificationMeta _createdAtUtcMeta = const VerificationMeta( + 'createdAtUtc', ); @override - late final GeneratedColumn isDeleted = GeneratedColumn( - 'is_deleted', + late final GeneratedColumn createdAtUtc = GeneratedColumn( + 'created_at_utc', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_deleted" IN (0, 1))', - ), - defaultValue: const Constant(false), + type: DriftSqlType.string, + requiredDuringInsert: true, ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', + static const VerificationMeta _resolvedAtUtcMeta = const VerificationMeta( + 'resolvedAtUtc', ); @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', + late final GeneratedColumn resolvedAtUtc = GeneratedColumn( + 'resolved_at_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( - 'createdAtLocal', - ); - @override - late final GeneratedColumn createdAtLocal = GeneratedColumn( - 'created_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( - 'updatedAtLocal', + static const VerificationMeta _resolutionMeta = const VerificationMeta( + 'resolution', ); @override - late final GeneratedColumn updatedAtLocal = GeneratedColumn( - 'updated_at_local', + late final GeneratedColumn resolution = GeneratedColumn( + 'resolution', aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); @override List get $columns => [ id, accountId, - provider, - providerCalendarId, - summary, - description, - primaryCalendar, - selected, - hidden, - readOnly, - backgroundColor, - foregroundColor, - colorId, - timeZone, - accessRole, - isDeleted, - rawJson, - createdAtLocal, - updatedAtLocal, + davCollectionId, + davObjectId, + baselineEtag, + baselineRawIcs, + localCandidateRawIcs, + remoteEtag, + remoteRawIcs, + conflictCode, + createdAtUtc, + resolvedAtUtc, + resolution, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'calendar_sources'; + static const String $name = 'dav_conflict_snapshots'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); @@ -7133,138 +6174,108 @@ class $CalendarSourcesTable extends CalendarSources } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('provider')) { + if (data.containsKey('dav_collection_id')) { context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), ); - } else if (isInserting) { - context.missing(_providerMeta); } - if (data.containsKey('provider_calendar_id')) { + if (data.containsKey('dav_object_id')) { context.handle( - _providerCalendarIdMeta, - providerCalendarId.isAcceptableOrUnknown( - data['provider_calendar_id']!, - _providerCalendarIdMeta, + _davObjectIdMeta, + davObjectId.isAcceptableOrUnknown( + data['dav_object_id']!, + _davObjectIdMeta, ), ); - } else if (isInserting) { - context.missing(_providerCalendarIdMeta); } - if (data.containsKey('summary')) { + if (data.containsKey('baseline_etag')) { context.handle( - _summaryMeta, - summary.isAcceptableOrUnknown(data['summary']!, _summaryMeta), + _baselineEtagMeta, + baselineEtag.isAcceptableOrUnknown( + data['baseline_etag']!, + _baselineEtagMeta, + ), ); - } else if (isInserting) { - context.missing(_summaryMeta); } - if (data.containsKey('description')) { + if (data.containsKey('baseline_raw_ics')) { context.handle( - _descriptionMeta, - description.isAcceptableOrUnknown( - data['description']!, - _descriptionMeta, + _baselineRawIcsMeta, + baselineRawIcs.isAcceptableOrUnknown( + data['baseline_raw_ics']!, + _baselineRawIcsMeta, ), ); + } else if (isInserting) { + context.missing(_baselineRawIcsMeta); } - if (data.containsKey('primary_calendar')) { + if (data.containsKey('local_candidate_raw_ics')) { context.handle( - _primaryCalendarMeta, - primaryCalendar.isAcceptableOrUnknown( - data['primary_calendar']!, - _primaryCalendarMeta, + _localCandidateRawIcsMeta, + localCandidateRawIcs.isAcceptableOrUnknown( + data['local_candidate_raw_ics']!, + _localCandidateRawIcsMeta, ), ); + } else if (isInserting) { + context.missing(_localCandidateRawIcsMeta); } - if (data.containsKey('selected')) { + if (data.containsKey('remote_etag')) { context.handle( - _selectedMeta, - selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta), + _remoteEtagMeta, + remoteEtag.isAcceptableOrUnknown(data['remote_etag']!, _remoteEtagMeta), ); } - if (data.containsKey('hidden')) { + if (data.containsKey('remote_raw_ics')) { context.handle( - _hiddenMeta, - hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta), + _remoteRawIcsMeta, + remoteRawIcs.isAcceptableOrUnknown( + data['remote_raw_ics']!, + _remoteRawIcsMeta, + ), ); + } else if (isInserting) { + context.missing(_remoteRawIcsMeta); } - if (data.containsKey('read_only')) { + if (data.containsKey('conflict_code')) { context.handle( - _readOnlyMeta, - readOnly.isAcceptableOrUnknown(data['read_only']!, _readOnlyMeta), + _conflictCodeMeta, + conflictCode.isAcceptableOrUnknown( + data['conflict_code']!, + _conflictCodeMeta, + ), ); + } else if (isInserting) { + context.missing(_conflictCodeMeta); } - if (data.containsKey('background_color')) { + if (data.containsKey('created_at_utc')) { context.handle( - _backgroundColorMeta, - backgroundColor.isAcceptableOrUnknown( - data['background_color']!, - _backgroundColorMeta, + _createdAtUtcMeta, + createdAtUtc.isAcceptableOrUnknown( + data['created_at_utc']!, + _createdAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_createdAtUtcMeta); } - if (data.containsKey('foreground_color')) { + if (data.containsKey('resolved_at_utc')) { context.handle( - _foregroundColorMeta, - foregroundColor.isAcceptableOrUnknown( - data['foreground_color']!, - _foregroundColorMeta, - ), - ); - } - if (data.containsKey('color_id')) { - context.handle( - _colorIdMeta, - colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), - ); - } - if (data.containsKey('time_zone')) { - context.handle( - _timeZoneMeta, - timeZone.isAcceptableOrUnknown(data['time_zone']!, _timeZoneMeta), - ); - } - if (data.containsKey('access_role')) { - context.handle( - _accessRoleMeta, - accessRole.isAcceptableOrUnknown(data['access_role']!, _accessRoleMeta), - ); - } - if (data.containsKey('is_deleted')) { - context.handle( - _isDeletedMeta, - isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta), - ); - } - if (data.containsKey('raw_json')) { - context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), - ); - } - if (data.containsKey('created_at_local')) { - context.handle( - _createdAtLocalMeta, - createdAtLocal.isAcceptableOrUnknown( - data['created_at_local']!, - _createdAtLocalMeta, + _resolvedAtUtcMeta, + resolvedAtUtc.isAcceptableOrUnknown( + data['resolved_at_utc']!, + _resolvedAtUtcMeta, ), ); - } else if (isInserting) { - context.missing(_createdAtLocalMeta); } - if (data.containsKey('updated_at_local')) { + if (data.containsKey('resolution')) { context.handle( - _updatedAtLocalMeta, - updatedAtLocal.isAcceptableOrUnknown( - data['updated_at_local']!, - _updatedAtLocalMeta, - ), + _resolutionMeta, + resolution.isAcceptableOrUnknown(data['resolution']!, _resolutionMeta), ); - } else if (isInserting) { - context.missing(_updatedAtLocalMeta); } return context; } @@ -7272,9 +6283,9 @@ class $CalendarSourcesTable extends CalendarSources @override Set get $primaryKey => {id}; @override - CalendarSource map(Map data, {String? tablePrefix}) { + DavConflictSnapshot map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarSource( + return DavConflictSnapshot( id: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}id'], @@ -7283,228 +6294,171 @@ class $CalendarSourcesTable extends CalendarSources DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - )!, - providerCalendarId: attachedDatabase.typeMapping.read( + davCollectionId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_calendar_id'], - )!, - summary: attachedDatabase.typeMapping.read( + data['${effectivePrefix}dav_collection_id'], + ), + davObjectId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}summary'], - )!, - description: attachedDatabase.typeMapping.read( + data['${effectivePrefix}dav_object_id'], + ), + baselineEtag: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}description'], + data['${effectivePrefix}baseline_etag'], ), - primaryCalendar: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}primary_calendar'], - )!, - selected: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}selected'], - )!, - hidden: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}hidden'], + baselineRawIcs: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_raw_ics'], )!, - readOnly: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}read_only'], + localCandidateRawIcs: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_candidate_raw_ics'], )!, - backgroundColor: attachedDatabase.typeMapping.read( + remoteEtag: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}background_color'], + data['${effectivePrefix}remote_etag'], ), - foregroundColor: attachedDatabase.typeMapping.read( + remoteRawIcs: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}foreground_color'], - ), - colorId: attachedDatabase.typeMapping.read( + data['${effectivePrefix}remote_raw_ics'], + )!, + conflictCode: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}color_id'], - ), - timeZone: attachedDatabase.typeMapping.read( + data['${effectivePrefix}conflict_code'], + )!, + createdAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}time_zone'], - ), - accessRole: attachedDatabase.typeMapping.read( + data['${effectivePrefix}created_at_utc'], + )!, + resolvedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}access_role'], + data['${effectivePrefix}resolved_at_utc'], ), - isDeleted: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}is_deleted'], - )!, - rawJson: attachedDatabase.typeMapping.read( + resolution: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}raw_json'], + data['${effectivePrefix}resolution'], ), - createdAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}created_at_local'], - )!, - updatedAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}updated_at_local'], - )!, ); } @override - $CalendarSourcesTable createAlias(String alias) { - return $CalendarSourcesTable(attachedDatabase, alias); + $DavConflictSnapshotsTable createAlias(String alias) { + return $DavConflictSnapshotsTable(attachedDatabase, alias); } } -class CalendarSource extends DataClass implements Insertable { +class DavConflictSnapshot extends DataClass + implements Insertable { final String id; final String accountId; - final String provider; - final String providerCalendarId; - final String summary; - final String? description; - final bool primaryCalendar; - final bool selected; - final bool hidden; - final bool readOnly; - final String? backgroundColor; - final String? foregroundColor; - final String? colorId; - final String? timeZone; - final String? accessRole; - final bool isDeleted; - final String? rawJson; - final int createdAtLocal; - final int updatedAtLocal; - const CalendarSource({ + final String? davCollectionId; + final String? davObjectId; + final String? baselineEtag; + final String baselineRawIcs; + final String localCandidateRawIcs; + final String? remoteEtag; + final String remoteRawIcs; + final String conflictCode; + final String createdAtUtc; + final String? resolvedAtUtc; + final String? resolution; + const DavConflictSnapshot({ required this.id, required this.accountId, - required this.provider, - required this.providerCalendarId, - required this.summary, - this.description, - required this.primaryCalendar, - required this.selected, - required this.hidden, - required this.readOnly, - this.backgroundColor, - this.foregroundColor, - this.colorId, - this.timeZone, - this.accessRole, - required this.isDeleted, - this.rawJson, - required this.createdAtLocal, - required this.updatedAtLocal, + this.davCollectionId, + this.davObjectId, + this.baselineEtag, + required this.baselineRawIcs, + required this.localCandidateRawIcs, + this.remoteEtag, + required this.remoteRawIcs, + required this.conflictCode, + required this.createdAtUtc, + this.resolvedAtUtc, + this.resolution, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); map['account_id'] = Variable(accountId); - map['provider'] = Variable(provider); - map['provider_calendar_id'] = Variable(providerCalendarId); - map['summary'] = Variable(summary); - if (!nullToAbsent || description != null) { - map['description'] = Variable(description); - } - map['primary_calendar'] = Variable(primaryCalendar); - map['selected'] = Variable(selected); - map['hidden'] = Variable(hidden); - map['read_only'] = Variable(readOnly); - if (!nullToAbsent || backgroundColor != null) { - map['background_color'] = Variable(backgroundColor); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); } - if (!nullToAbsent || foregroundColor != null) { - map['foreground_color'] = Variable(foregroundColor); + if (!nullToAbsent || davObjectId != null) { + map['dav_object_id'] = Variable(davObjectId); } - if (!nullToAbsent || colorId != null) { - map['color_id'] = Variable(colorId); + if (!nullToAbsent || baselineEtag != null) { + map['baseline_etag'] = Variable(baselineEtag); } - if (!nullToAbsent || timeZone != null) { - map['time_zone'] = Variable(timeZone); + map['baseline_raw_ics'] = Variable(baselineRawIcs); + map['local_candidate_raw_ics'] = Variable(localCandidateRawIcs); + if (!nullToAbsent || remoteEtag != null) { + map['remote_etag'] = Variable(remoteEtag); } - if (!nullToAbsent || accessRole != null) { - map['access_role'] = Variable(accessRole); + map['remote_raw_ics'] = Variable(remoteRawIcs); + map['conflict_code'] = Variable(conflictCode); + map['created_at_utc'] = Variable(createdAtUtc); + if (!nullToAbsent || resolvedAtUtc != null) { + map['resolved_at_utc'] = Variable(resolvedAtUtc); } - map['is_deleted'] = Variable(isDeleted); - if (!nullToAbsent || rawJson != null) { - map['raw_json'] = Variable(rawJson); + if (!nullToAbsent || resolution != null) { + map['resolution'] = Variable(resolution); } - map['created_at_local'] = Variable(createdAtLocal); - map['updated_at_local'] = Variable(updatedAtLocal); return map; } - CalendarSourcesCompanion toCompanion(bool nullToAbsent) { - return CalendarSourcesCompanion( + DavConflictSnapshotsCompanion toCompanion(bool nullToAbsent) { + return DavConflictSnapshotsCompanion( id: Value(id), accountId: Value(accountId), - provider: Value(provider), - providerCalendarId: Value(providerCalendarId), - summary: Value(summary), - description: description == null && nullToAbsent - ? const Value.absent() - : Value(description), - primaryCalendar: Value(primaryCalendar), - selected: Value(selected), - hidden: Value(hidden), - readOnly: Value(readOnly), - backgroundColor: backgroundColor == null && nullToAbsent + davCollectionId: davCollectionId == null && nullToAbsent ? const Value.absent() - : Value(backgroundColor), - foregroundColor: foregroundColor == null && nullToAbsent + : Value(davCollectionId), + davObjectId: davObjectId == null && nullToAbsent ? const Value.absent() - : Value(foregroundColor), - colorId: colorId == null && nullToAbsent + : Value(davObjectId), + baselineEtag: baselineEtag == null && nullToAbsent ? const Value.absent() - : Value(colorId), - timeZone: timeZone == null && nullToAbsent + : Value(baselineEtag), + baselineRawIcs: Value(baselineRawIcs), + localCandidateRawIcs: Value(localCandidateRawIcs), + remoteEtag: remoteEtag == null && nullToAbsent ? const Value.absent() - : Value(timeZone), - accessRole: accessRole == null && nullToAbsent + : Value(remoteEtag), + remoteRawIcs: Value(remoteRawIcs), + conflictCode: Value(conflictCode), + createdAtUtc: Value(createdAtUtc), + resolvedAtUtc: resolvedAtUtc == null && nullToAbsent ? const Value.absent() - : Value(accessRole), - isDeleted: Value(isDeleted), - rawJson: rawJson == null && nullToAbsent + : Value(resolvedAtUtc), + resolution: resolution == null && nullToAbsent ? const Value.absent() - : Value(rawJson), - createdAtLocal: Value(createdAtLocal), - updatedAtLocal: Value(updatedAtLocal), + : Value(resolution), ); } - factory CalendarSource.fromJson( + factory DavConflictSnapshot.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarSource( + return DavConflictSnapshot( id: serializer.fromJson(json['id']), accountId: serializer.fromJson(json['accountId']), - provider: serializer.fromJson(json['provider']), - providerCalendarId: serializer.fromJson( - json['providerCalendarId'], + davCollectionId: serializer.fromJson(json['davCollectionId']), + davObjectId: serializer.fromJson(json['davObjectId']), + baselineEtag: serializer.fromJson(json['baselineEtag']), + baselineRawIcs: serializer.fromJson(json['baselineRawIcs']), + localCandidateRawIcs: serializer.fromJson( + json['localCandidateRawIcs'], ), - summary: serializer.fromJson(json['summary']), - description: serializer.fromJson(json['description']), - primaryCalendar: serializer.fromJson(json['primaryCalendar']), - selected: serializer.fromJson(json['selected']), - hidden: serializer.fromJson(json['hidden']), - readOnly: serializer.fromJson(json['readOnly']), - backgroundColor: serializer.fromJson(json['backgroundColor']), - foregroundColor: serializer.fromJson(json['foregroundColor']), - colorId: serializer.fromJson(json['colorId']), - timeZone: serializer.fromJson(json['timeZone']), - accessRole: serializer.fromJson(json['accessRole']), - isDeleted: serializer.fromJson(json['isDeleted']), - rawJson: serializer.fromJson(json['rawJson']), - createdAtLocal: serializer.fromJson(json['createdAtLocal']), - updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + remoteEtag: serializer.fromJson(json['remoteEtag']), + remoteRawIcs: serializer.fromJson(json['remoteRawIcs']), + conflictCode: serializer.fromJson(json['conflictCode']), + createdAtUtc: serializer.fromJson(json['createdAtUtc']), + resolvedAtUtc: serializer.fromJson(json['resolvedAtUtc']), + resolution: serializer.fromJson(json['resolution']), ); } @override @@ -7513,133 +6467,109 @@ class CalendarSource extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'accountId': serializer.toJson(accountId), - 'provider': serializer.toJson(provider), - 'providerCalendarId': serializer.toJson(providerCalendarId), - 'summary': serializer.toJson(summary), - 'description': serializer.toJson(description), - 'primaryCalendar': serializer.toJson(primaryCalendar), - 'selected': serializer.toJson(selected), - 'hidden': serializer.toJson(hidden), - 'readOnly': serializer.toJson(readOnly), - 'backgroundColor': serializer.toJson(backgroundColor), - 'foregroundColor': serializer.toJson(foregroundColor), - 'colorId': serializer.toJson(colorId), - 'timeZone': serializer.toJson(timeZone), - 'accessRole': serializer.toJson(accessRole), - 'isDeleted': serializer.toJson(isDeleted), - 'rawJson': serializer.toJson(rawJson), - 'createdAtLocal': serializer.toJson(createdAtLocal), - 'updatedAtLocal': serializer.toJson(updatedAtLocal), + 'davCollectionId': serializer.toJson(davCollectionId), + 'davObjectId': serializer.toJson(davObjectId), + 'baselineEtag': serializer.toJson(baselineEtag), + 'baselineRawIcs': serializer.toJson(baselineRawIcs), + 'localCandidateRawIcs': serializer.toJson(localCandidateRawIcs), + 'remoteEtag': serializer.toJson(remoteEtag), + 'remoteRawIcs': serializer.toJson(remoteRawIcs), + 'conflictCode': serializer.toJson(conflictCode), + 'createdAtUtc': serializer.toJson(createdAtUtc), + 'resolvedAtUtc': serializer.toJson(resolvedAtUtc), + 'resolution': serializer.toJson(resolution), }; } - CalendarSource copyWith({ + DavConflictSnapshot copyWith({ String? id, String? accountId, - String? provider, - String? providerCalendarId, - String? summary, - Value description = const Value.absent(), - bool? primaryCalendar, - bool? selected, - bool? hidden, - bool? readOnly, - Value backgroundColor = const Value.absent(), - Value foregroundColor = const Value.absent(), - Value colorId = const Value.absent(), - Value timeZone = const Value.absent(), - Value accessRole = const Value.absent(), - bool? isDeleted, - Value rawJson = const Value.absent(), - int? createdAtLocal, - int? updatedAtLocal, - }) => CalendarSource( + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value baselineEtag = const Value.absent(), + String? baselineRawIcs, + String? localCandidateRawIcs, + Value remoteEtag = const Value.absent(), + String? remoteRawIcs, + String? conflictCode, + String? createdAtUtc, + Value resolvedAtUtc = const Value.absent(), + Value resolution = const Value.absent(), + }) => DavConflictSnapshot( id: id ?? this.id, accountId: accountId ?? this.accountId, - provider: provider ?? this.provider, - providerCalendarId: providerCalendarId ?? this.providerCalendarId, - summary: summary ?? this.summary, - description: description.present ? description.value : this.description, - primaryCalendar: primaryCalendar ?? this.primaryCalendar, - selected: selected ?? this.selected, - hidden: hidden ?? this.hidden, - readOnly: readOnly ?? this.readOnly, - backgroundColor: backgroundColor.present - ? backgroundColor.value - : this.backgroundColor, - foregroundColor: foregroundColor.present - ? foregroundColor.value - : this.foregroundColor, - colorId: colorId.present ? colorId.value : this.colorId, - timeZone: timeZone.present ? timeZone.value : this.timeZone, - accessRole: accessRole.present ? accessRole.value : this.accessRole, - isDeleted: isDeleted ?? this.isDeleted, - rawJson: rawJson.present ? rawJson.value : this.rawJson, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + davObjectId: davObjectId.present ? davObjectId.value : this.davObjectId, + baselineEtag: baselineEtag.present ? baselineEtag.value : this.baselineEtag, + baselineRawIcs: baselineRawIcs ?? this.baselineRawIcs, + localCandidateRawIcs: localCandidateRawIcs ?? this.localCandidateRawIcs, + remoteEtag: remoteEtag.present ? remoteEtag.value : this.remoteEtag, + remoteRawIcs: remoteRawIcs ?? this.remoteRawIcs, + conflictCode: conflictCode ?? this.conflictCode, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + resolvedAtUtc: resolvedAtUtc.present + ? resolvedAtUtc.value + : this.resolvedAtUtc, + resolution: resolution.present ? resolution.value : this.resolution, ); - CalendarSource copyWithCompanion(CalendarSourcesCompanion data) { - return CalendarSource( + DavConflictSnapshot copyWithCompanion(DavConflictSnapshotsCompanion data) { + return DavConflictSnapshot( id: data.id.present ? data.id.value : this.id, accountId: data.accountId.present ? data.accountId.value : this.accountId, - provider: data.provider.present ? data.provider.value : this.provider, - providerCalendarId: data.providerCalendarId.present - ? data.providerCalendarId.value - : this.providerCalendarId, - summary: data.summary.present ? data.summary.value : this.summary, - description: data.description.present - ? data.description.value - : this.description, - primaryCalendar: data.primaryCalendar.present - ? data.primaryCalendar.value - : this.primaryCalendar, - selected: data.selected.present ? data.selected.value : this.selected, - hidden: data.hidden.present ? data.hidden.value : this.hidden, - readOnly: data.readOnly.present ? data.readOnly.value : this.readOnly, - backgroundColor: data.backgroundColor.present - ? data.backgroundColor.value - : this.backgroundColor, - foregroundColor: data.foregroundColor.present - ? data.foregroundColor.value - : this.foregroundColor, - colorId: data.colorId.present ? data.colorId.value : this.colorId, - timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, - accessRole: data.accessRole.present - ? data.accessRole.value - : this.accessRole, - isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - createdAtLocal: data.createdAtLocal.present - ? data.createdAtLocal.value - : this.createdAtLocal, - updatedAtLocal: data.updatedAtLocal.present - ? data.updatedAtLocal.value - : this.updatedAtLocal, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + davObjectId: data.davObjectId.present + ? data.davObjectId.value + : this.davObjectId, + baselineEtag: data.baselineEtag.present + ? data.baselineEtag.value + : this.baselineEtag, + baselineRawIcs: data.baselineRawIcs.present + ? data.baselineRawIcs.value + : this.baselineRawIcs, + localCandidateRawIcs: data.localCandidateRawIcs.present + ? data.localCandidateRawIcs.value + : this.localCandidateRawIcs, + remoteEtag: data.remoteEtag.present + ? data.remoteEtag.value + : this.remoteEtag, + remoteRawIcs: data.remoteRawIcs.present + ? data.remoteRawIcs.value + : this.remoteRawIcs, + conflictCode: data.conflictCode.present + ? data.conflictCode.value + : this.conflictCode, + createdAtUtc: data.createdAtUtc.present + ? data.createdAtUtc.value + : this.createdAtUtc, + resolvedAtUtc: data.resolvedAtUtc.present + ? data.resolvedAtUtc.value + : this.resolvedAtUtc, + resolution: data.resolution.present + ? data.resolution.value + : this.resolution, ); } @override String toString() { - return (StringBuffer('CalendarSource(') + return (StringBuffer('DavConflictSnapshot(') ..write('id: $id, ') ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('summary: $summary, ') - ..write('description: $description, ') - ..write('primaryCalendar: $primaryCalendar, ') - ..write('selected: $selected, ') - ..write('hidden: $hidden, ') - ..write('readOnly: $readOnly, ') - ..write('backgroundColor: $backgroundColor, ') - ..write('foregroundColor: $foregroundColor, ') - ..write('colorId: $colorId, ') - ..write('timeZone: $timeZone, ') - ..write('accessRole: $accessRole, ') - ..write('isDeleted: $isDeleted, ') - ..write('rawJson: $rawJson, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('baselineEtag: $baselineEtag, ') + ..write('baselineRawIcs: $baselineRawIcs, ') + ..write('localCandidateRawIcs: $localCandidateRawIcs, ') + ..write('remoteEtag: $remoteEtag, ') + ..write('remoteRawIcs: $remoteRawIcs, ') + ..write('conflictCode: $conflictCode, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('resolvedAtUtc: $resolvedAtUtc, ') + ..write('resolution: $resolution') ..write(')')) .toString(); } @@ -7648,209 +6578,156 @@ class CalendarSource extends DataClass implements Insertable { int get hashCode => Object.hash( id, accountId, - provider, - providerCalendarId, - summary, - description, - primaryCalendar, - selected, - hidden, - readOnly, - backgroundColor, - foregroundColor, - colorId, - timeZone, - accessRole, - isDeleted, - rawJson, - createdAtLocal, - updatedAtLocal, + davCollectionId, + davObjectId, + baselineEtag, + baselineRawIcs, + localCandidateRawIcs, + remoteEtag, + remoteRawIcs, + conflictCode, + createdAtUtc, + resolvedAtUtc, + resolution, ); @override bool operator ==(Object other) => identical(this, other) || - (other is CalendarSource && + (other is DavConflictSnapshot && other.id == this.id && other.accountId == this.accountId && - other.provider == this.provider && - other.providerCalendarId == this.providerCalendarId && - other.summary == this.summary && - other.description == this.description && - other.primaryCalendar == this.primaryCalendar && - other.selected == this.selected && - other.hidden == this.hidden && - other.readOnly == this.readOnly && - other.backgroundColor == this.backgroundColor && - other.foregroundColor == this.foregroundColor && - other.colorId == this.colorId && - other.timeZone == this.timeZone && - other.accessRole == this.accessRole && - other.isDeleted == this.isDeleted && - other.rawJson == this.rawJson && - other.createdAtLocal == this.createdAtLocal && - other.updatedAtLocal == this.updatedAtLocal); + other.davCollectionId == this.davCollectionId && + other.davObjectId == this.davObjectId && + other.baselineEtag == this.baselineEtag && + other.baselineRawIcs == this.baselineRawIcs && + other.localCandidateRawIcs == this.localCandidateRawIcs && + other.remoteEtag == this.remoteEtag && + other.remoteRawIcs == this.remoteRawIcs && + other.conflictCode == this.conflictCode && + other.createdAtUtc == this.createdAtUtc && + other.resolvedAtUtc == this.resolvedAtUtc && + other.resolution == this.resolution); } -class CalendarSourcesCompanion extends UpdateCompanion { +class DavConflictSnapshotsCompanion + extends UpdateCompanion { final Value id; final Value accountId; - final Value provider; - final Value providerCalendarId; - final Value summary; - final Value description; - final Value primaryCalendar; - final Value selected; - final Value hidden; - final Value readOnly; - final Value backgroundColor; - final Value foregroundColor; - final Value colorId; - final Value timeZone; - final Value accessRole; - final Value isDeleted; - final Value rawJson; - final Value createdAtLocal; - final Value updatedAtLocal; + final Value davCollectionId; + final Value davObjectId; + final Value baselineEtag; + final Value baselineRawIcs; + final Value localCandidateRawIcs; + final Value remoteEtag; + final Value remoteRawIcs; + final Value conflictCode; + final Value createdAtUtc; + final Value resolvedAtUtc; + final Value resolution; final Value rowid; - const CalendarSourcesCompanion({ + const DavConflictSnapshotsCompanion({ this.id = const Value.absent(), this.accountId = const Value.absent(), - this.provider = const Value.absent(), - this.providerCalendarId = const Value.absent(), - this.summary = const Value.absent(), - this.description = const Value.absent(), - this.primaryCalendar = const Value.absent(), - this.selected = const Value.absent(), - this.hidden = const Value.absent(), - this.readOnly = const Value.absent(), - this.backgroundColor = const Value.absent(), - this.foregroundColor = const Value.absent(), - this.colorId = const Value.absent(), - this.timeZone = const Value.absent(), - this.accessRole = const Value.absent(), - this.isDeleted = const Value.absent(), - this.rawJson = const Value.absent(), - this.createdAtLocal = const Value.absent(), - this.updatedAtLocal = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.baselineEtag = const Value.absent(), + this.baselineRawIcs = const Value.absent(), + this.localCandidateRawIcs = const Value.absent(), + this.remoteEtag = const Value.absent(), + this.remoteRawIcs = const Value.absent(), + this.conflictCode = const Value.absent(), + this.createdAtUtc = const Value.absent(), + this.resolvedAtUtc = const Value.absent(), + this.resolution = const Value.absent(), this.rowid = const Value.absent(), }); - CalendarSourcesCompanion.insert({ + DavConflictSnapshotsCompanion.insert({ required String id, required String accountId, - required String provider, - required String providerCalendarId, - required String summary, - this.description = const Value.absent(), - this.primaryCalendar = const Value.absent(), - this.selected = const Value.absent(), - this.hidden = const Value.absent(), - this.readOnly = const Value.absent(), - this.backgroundColor = const Value.absent(), - this.foregroundColor = const Value.absent(), - this.colorId = const Value.absent(), - this.timeZone = const Value.absent(), - this.accessRole = const Value.absent(), - this.isDeleted = const Value.absent(), - this.rawJson = const Value.absent(), - required int createdAtLocal, - required int updatedAtLocal, + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.baselineEtag = const Value.absent(), + required String baselineRawIcs, + required String localCandidateRawIcs, + this.remoteEtag = const Value.absent(), + required String remoteRawIcs, + required String conflictCode, + required String createdAtUtc, + this.resolvedAtUtc = const Value.absent(), + this.resolution = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), accountId = Value(accountId), - provider = Value(provider), - providerCalendarId = Value(providerCalendarId), - summary = Value(summary), - createdAtLocal = Value(createdAtLocal), - updatedAtLocal = Value(updatedAtLocal); - static Insertable custom({ + baselineRawIcs = Value(baselineRawIcs), + localCandidateRawIcs = Value(localCandidateRawIcs), + remoteRawIcs = Value(remoteRawIcs), + conflictCode = Value(conflictCode), + createdAtUtc = Value(createdAtUtc); + static Insertable custom({ Expression? id, Expression? accountId, - Expression? provider, - Expression? providerCalendarId, - Expression? summary, - Expression? description, - Expression? primaryCalendar, - Expression? selected, - Expression? hidden, - Expression? readOnly, - Expression? backgroundColor, - Expression? foregroundColor, - Expression? colorId, - Expression? timeZone, - Expression? accessRole, - Expression? isDeleted, - Expression? rawJson, - Expression? createdAtLocal, - Expression? updatedAtLocal, + Expression? davCollectionId, + Expression? davObjectId, + Expression? baselineEtag, + Expression? baselineRawIcs, + Expression? localCandidateRawIcs, + Expression? remoteEtag, + Expression? remoteRawIcs, + Expression? conflictCode, + Expression? createdAtUtc, + Expression? resolvedAtUtc, + Expression? resolution, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (accountId != null) 'account_id': accountId, - if (provider != null) 'provider': provider, - if (providerCalendarId != null) - 'provider_calendar_id': providerCalendarId, - if (summary != null) 'summary': summary, - if (description != null) 'description': description, - if (primaryCalendar != null) 'primary_calendar': primaryCalendar, - if (selected != null) 'selected': selected, - if (hidden != null) 'hidden': hidden, - if (readOnly != null) 'read_only': readOnly, - if (backgroundColor != null) 'background_color': backgroundColor, - if (foregroundColor != null) 'foreground_color': foregroundColor, - if (colorId != null) 'color_id': colorId, - if (timeZone != null) 'time_zone': timeZone, - if (accessRole != null) 'access_role': accessRole, - if (isDeleted != null) 'is_deleted': isDeleted, - if (rawJson != null) 'raw_json': rawJson, - if (createdAtLocal != null) 'created_at_local': createdAtLocal, - if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (davObjectId != null) 'dav_object_id': davObjectId, + if (baselineEtag != null) 'baseline_etag': baselineEtag, + if (baselineRawIcs != null) 'baseline_raw_ics': baselineRawIcs, + if (localCandidateRawIcs != null) + 'local_candidate_raw_ics': localCandidateRawIcs, + if (remoteEtag != null) 'remote_etag': remoteEtag, + if (remoteRawIcs != null) 'remote_raw_ics': remoteRawIcs, + if (conflictCode != null) 'conflict_code': conflictCode, + if (createdAtUtc != null) 'created_at_utc': createdAtUtc, + if (resolvedAtUtc != null) 'resolved_at_utc': resolvedAtUtc, + if (resolution != null) 'resolution': resolution, if (rowid != null) 'rowid': rowid, }); } - CalendarSourcesCompanion copyWith({ + DavConflictSnapshotsCompanion copyWith({ Value? id, Value? accountId, - Value? provider, - Value? providerCalendarId, - Value? summary, - Value? description, - Value? primaryCalendar, - Value? selected, - Value? hidden, - Value? readOnly, - Value? backgroundColor, - Value? foregroundColor, - Value? colorId, - Value? timeZone, - Value? accessRole, - Value? isDeleted, - Value? rawJson, - Value? createdAtLocal, - Value? updatedAtLocal, + Value? davCollectionId, + Value? davObjectId, + Value? baselineEtag, + Value? baselineRawIcs, + Value? localCandidateRawIcs, + Value? remoteEtag, + Value? remoteRawIcs, + Value? conflictCode, + Value? createdAtUtc, + Value? resolvedAtUtc, + Value? resolution, Value? rowid, }) { - return CalendarSourcesCompanion( + return DavConflictSnapshotsCompanion( id: id ?? this.id, accountId: accountId ?? this.accountId, - provider: provider ?? this.provider, - providerCalendarId: providerCalendarId ?? this.providerCalendarId, - summary: summary ?? this.summary, - description: description ?? this.description, - primaryCalendar: primaryCalendar ?? this.primaryCalendar, - selected: selected ?? this.selected, - hidden: hidden ?? this.hidden, - readOnly: readOnly ?? this.readOnly, - backgroundColor: backgroundColor ?? this.backgroundColor, - foregroundColor: foregroundColor ?? this.foregroundColor, - colorId: colorId ?? this.colorId, - timeZone: timeZone ?? this.timeZone, - accessRole: accessRole ?? this.accessRole, - isDeleted: isDeleted ?? this.isDeleted, - rawJson: rawJson ?? this.rawJson, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + davCollectionId: davCollectionId ?? this.davCollectionId, + davObjectId: davObjectId ?? this.davObjectId, + baselineEtag: baselineEtag ?? this.baselineEtag, + baselineRawIcs: baselineRawIcs ?? this.baselineRawIcs, + localCandidateRawIcs: localCandidateRawIcs ?? this.localCandidateRawIcs, + remoteEtag: remoteEtag ?? this.remoteEtag, + remoteRawIcs: remoteRawIcs ?? this.remoteRawIcs, + conflictCode: conflictCode ?? this.conflictCode, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + resolvedAtUtc: resolvedAtUtc ?? this.resolvedAtUtc, + resolution: resolution ?? this.resolution, rowid: rowid ?? this.rowid, ); } @@ -7864,56 +6741,40 @@ class CalendarSourcesCompanion extends UpdateCompanion { if (accountId.present) { map['account_id'] = Variable(accountId.value); } - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (providerCalendarId.present) { - map['provider_calendar_id'] = Variable(providerCalendarId.value); - } - if (summary.present) { - map['summary'] = Variable(summary.value); - } - if (description.present) { - map['description'] = Variable(description.value); - } - if (primaryCalendar.present) { - map['primary_calendar'] = Variable(primaryCalendar.value); - } - if (selected.present) { - map['selected'] = Variable(selected.value); - } - if (hidden.present) { - map['hidden'] = Variable(hidden.value); + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); } - if (readOnly.present) { - map['read_only'] = Variable(readOnly.value); + if (davObjectId.present) { + map['dav_object_id'] = Variable(davObjectId.value); } - if (backgroundColor.present) { - map['background_color'] = Variable(backgroundColor.value); + if (baselineEtag.present) { + map['baseline_etag'] = Variable(baselineEtag.value); } - if (foregroundColor.present) { - map['foreground_color'] = Variable(foregroundColor.value); + if (baselineRawIcs.present) { + map['baseline_raw_ics'] = Variable(baselineRawIcs.value); } - if (colorId.present) { - map['color_id'] = Variable(colorId.value); + if (localCandidateRawIcs.present) { + map['local_candidate_raw_ics'] = Variable( + localCandidateRawIcs.value, + ); } - if (timeZone.present) { - map['time_zone'] = Variable(timeZone.value); + if (remoteEtag.present) { + map['remote_etag'] = Variable(remoteEtag.value); } - if (accessRole.present) { - map['access_role'] = Variable(accessRole.value); + if (remoteRawIcs.present) { + map['remote_raw_ics'] = Variable(remoteRawIcs.value); } - if (isDeleted.present) { - map['is_deleted'] = Variable(isDeleted.value); + if (conflictCode.present) { + map['conflict_code'] = Variable(conflictCode.value); } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); + if (createdAtUtc.present) { + map['created_at_utc'] = Variable(createdAtUtc.value); } - if (createdAtLocal.present) { - map['created_at_local'] = Variable(createdAtLocal.value); + if (resolvedAtUtc.present) { + map['resolved_at_utc'] = Variable(resolvedAtUtc.value); } - if (updatedAtLocal.present) { - map['updated_at_local'] = Variable(updatedAtLocal.value); + if (resolution.present) { + map['resolution'] = Variable(resolution.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -7923,47 +6784,32 @@ class CalendarSourcesCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('CalendarSourcesCompanion(') + return (StringBuffer('DavConflictSnapshotsCompanion(') ..write('id: $id, ') ..write('accountId: $accountId, ') - ..write('provider: $provider, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('summary: $summary, ') - ..write('description: $description, ') - ..write('primaryCalendar: $primaryCalendar, ') - ..write('selected: $selected, ') - ..write('hidden: $hidden, ') - ..write('readOnly: $readOnly, ') - ..write('backgroundColor: $backgroundColor, ') - ..write('foregroundColor: $foregroundColor, ') - ..write('colorId: $colorId, ') - ..write('timeZone: $timeZone, ') - ..write('accessRole: $accessRole, ') - ..write('isDeleted: $isDeleted, ') - ..write('rawJson: $rawJson, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('baselineEtag: $baselineEtag, ') + ..write('baselineRawIcs: $baselineRawIcs, ') + ..write('localCandidateRawIcs: $localCandidateRawIcs, ') + ..write('remoteEtag: $remoteEtag, ') + ..write('remoteRawIcs: $remoteRawIcs, ') + ..write('conflictCode: $conflictCode, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('resolvedAtUtc: $resolvedAtUtc, ') + ..write('resolution: $resolution, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $CalendarEventsTable extends CalendarEvents - with TableInfo<$CalendarEventsTable, CalendarEvent> { +class $TaskListsTable extends TaskLists + with TableInfo<$TaskListsTable, TaskList> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $CalendarEventsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); + $TaskListsTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _accountIdMeta = const VerificationMeta( 'accountId', ); @@ -7978,90 +6824,42 @@ class $CalendarEventsTable extends CalendarEvents 'REFERENCES accounts (id) ON DELETE CASCADE', ), ); - static const VerificationMeta _calendarSourceIdMeta = const VerificationMeta( - 'calendarSourceId', - ); - @override - late final GeneratedColumn calendarSourceId = GeneratedColumn( - 'calendar_source_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES calendar_sources (id) ON DELETE CASCADE', - ), - ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', - ); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _providerCalendarIdMeta = - const VerificationMeta('providerCalendarId'); - @override - late final GeneratedColumn providerCalendarId = - GeneratedColumn( - 'provider_calendar_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _providerEventIdMeta = const VerificationMeta( - 'providerEventId', + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', ); @override - late final GeneratedColumn providerEventId = GeneratedColumn( - 'provider_event_id', + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _providerRecurringEventIdMeta = - const VerificationMeta('providerRecurringEventId'); - @override - late final GeneratedColumn providerRecurringEventId = - GeneratedColumn( - 'provider_recurring_event_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _providerOriginalStartKeyMeta = - const VerificationMeta('providerOriginalStartKey'); - @override - late final GeneratedColumn providerOriginalStartKey = - GeneratedColumn( - 'provider_original_start_key', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _etagOrChangeKeyMeta = const VerificationMeta( - 'etagOrChangeKey', + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), ); + static const VerificationMeta _kindMeta = const VerificationMeta('kind'); @override - late final GeneratedColumn etagOrChangeKey = GeneratedColumn( - 'etag_or_change_key', + late final GeneratedColumn kind = GeneratedColumn( + 'kind', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _statusMeta = const VerificationMeta('status'); + static const VerificationMeta _etagMeta = const VerificationMeta('etag'); @override - late final GeneratedColumn status = GeneratedColumn( - 'status', + late final GeneratedColumn etag = GeneratedColumn( + 'etag', aliasedName, true, type: DriftSqlType.string, @@ -8076,432 +6874,215 @@ class $CalendarEventsTable extends CalendarEvents type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _descriptionMeta = const VerificationMeta( - 'description', + static const VerificationMeta _updatedUtcMeta = const VerificationMeta( + 'updatedUtc', ); @override - late final GeneratedColumn description = GeneratedColumn( - 'description', + late final GeneratedColumn updatedUtc = GeneratedColumn( + 'updated_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _locationMeta = const VerificationMeta( - 'location', + static const VerificationMeta _selfLinkMeta = const VerificationMeta( + 'selfLink', ); @override - late final GeneratedColumn location = GeneratedColumn( - 'location', + late final GeneratedColumn selfLink = GeneratedColumn( + 'self_link', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _allDayMeta = const VerificationMeta('allDay'); - @override - late final GeneratedColumn allDay = GeneratedColumn( - 'all_day', - aliasedName, - false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("all_day" IN (0, 1))', - ), - defaultValue: const Constant(false), - ); - static const VerificationMeta _startDateMeta = const VerificationMeta( - 'startDate', + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', ); @override - late final GeneratedColumn startDate = GeneratedColumn( - 'start_date', + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', aliasedName, - true, + false, type: DriftSqlType.string, - requiredDuringInsert: false, + requiredDuringInsert: true, ); - static const VerificationMeta _startDateTimeMeta = const VerificationMeta( - 'startDateTime', + static const VerificationMeta _providerListKindMeta = const VerificationMeta( + 'providerListKind', ); @override - late final GeneratedColumn startDateTime = GeneratedColumn( - 'start_date_time', + late final GeneratedColumn providerListKind = GeneratedColumn( + 'provider_list_kind', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _startTimeZoneMeta = const VerificationMeta( - 'startTimeZone', + static const VerificationMeta _isOwnerMeta = const VerificationMeta( + 'isOwner', ); @override - late final GeneratedColumn startTimeZone = GeneratedColumn( - 'start_time_zone', + late final GeneratedColumn isOwner = GeneratedColumn( + 'is_owner', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_owner" IN (0, 1))', + ), ); - static const VerificationMeta _endDateMeta = const VerificationMeta( - 'endDate', + static const VerificationMeta _isSharedMeta = const VerificationMeta( + 'isShared', ); @override - late final GeneratedColumn endDate = GeneratedColumn( - 'end_date', + late final GeneratedColumn isShared = GeneratedColumn( + 'is_shared', aliasedName, true, - type: DriftSqlType.string, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_shared" IN (0, 1))', + ), ); - static const VerificationMeta _endDateTimeMeta = const VerificationMeta( - 'endDateTime', + static const VerificationMeta _deltaLinkMeta = const VerificationMeta( + 'deltaLink', ); @override - late final GeneratedColumn endDateTime = GeneratedColumn( - 'end_date_time', + late final GeneratedColumn deltaLink = GeneratedColumn( + 'delta_link', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _endTimeZoneMeta = const VerificationMeta( - 'endTimeZone', - ); + static const VerificationMeta _providerMetadataJsonMeta = + const VerificationMeta('providerMetadataJson'); @override - late final GeneratedColumn endTimeZone = GeneratedColumn( - 'end_time_zone', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _recurrenceJsonMeta = const VerificationMeta( - 'recurrenceJson', - ); - @override - late final GeneratedColumn recurrenceJson = GeneratedColumn( - 'recurrence_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _remindersJsonMeta = const VerificationMeta( - 'remindersJson', - ); - @override - late final GeneratedColumn remindersJson = GeneratedColumn( - 'reminders_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _attendeesJsonMeta = const VerificationMeta( - 'attendeesJson', - ); - @override - late final GeneratedColumn attendeesJson = GeneratedColumn( - 'attendees_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _categoriesJsonMeta = const VerificationMeta( - 'categoriesJson', - ); - @override - late final GeneratedColumn categoriesJson = GeneratedColumn( - 'categories_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _organizerJsonMeta = const VerificationMeta( - 'organizerJson', - ); - @override - late final GeneratedColumn organizerJson = GeneratedColumn( - 'organizer_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _creatorJsonMeta = const VerificationMeta( - 'creatorJson', - ); - @override - late final GeneratedColumn creatorJson = GeneratedColumn( - 'creator_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _colorIdMeta = const VerificationMeta( - 'colorId', - ); - @override - late final GeneratedColumn colorId = GeneratedColumn( - 'color_id', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _colorHexMeta = const VerificationMeta( - 'colorHex', - ); - @override - late final GeneratedColumn colorHex = GeneratedColumn( - 'color_hex', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _visibilityMeta = const VerificationMeta( - 'visibility', - ); - @override - late final GeneratedColumn visibility = GeneratedColumn( - 'visibility', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _transparencyOrShowAsMeta = - const VerificationMeta('transparencyOrShowAs'); - @override - late final GeneratedColumn transparencyOrShowAs = + late final GeneratedColumn providerMetadataJson = GeneratedColumn( - 'transparency_or_show_as', + 'provider_metadata_json', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _eventTypeMeta = const VerificationMeta( - 'eventType', - ); - @override - late final GeneratedColumn eventType = GeneratedColumn( - 'event_type', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _webLinkMeta = const VerificationMeta( - 'webLink', - ); - @override - late final GeneratedColumn webLink = GeneratedColumn( - 'web_link', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _conferenceJsonMeta = const VerificationMeta( - 'conferenceJson', - ); - @override - late final GeneratedColumn conferenceJson = GeneratedColumn( - 'conference_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _attachmentsJsonMeta = const VerificationMeta( - 'attachmentsJson', - ); - @override - late final GeneratedColumn attachmentsJson = GeneratedColumn( - 'attachments_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _isCancelledMeta = const VerificationMeta( - 'isCancelled', + static const VerificationMeta _serverMissingMeta = const VerificationMeta( + 'serverMissing', ); @override - late final GeneratedColumn isCancelled = GeneratedColumn( - 'is_cancelled', + late final GeneratedColumn serverMissing = GeneratedColumn( + 'server_missing', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_cancelled" IN (0, 1))', + 'CHECK ("server_missing" IN (0, 1))', ), defaultValue: const Constant(false), ); - static const VerificationMeta _isDeletedMeta = const VerificationMeta( - 'isDeleted', + static const VerificationMeta _localDirtyMeta = const VerificationMeta( + 'localDirty', ); @override - late final GeneratedColumn isDeleted = GeneratedColumn( - 'is_deleted', + late final GeneratedColumn localDirty = GeneratedColumn( + 'local_dirty', aliasedName, false, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_deleted" IN (0, 1))', + 'CHECK ("local_dirty" IN (0, 1))', ), defaultValue: const Constant(false), ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', - ); - @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _createdAtServerMeta = const VerificationMeta( - 'createdAtServer', + static const VerificationMeta _pendingDeleteMeta = const VerificationMeta( + 'pendingDelete', ); @override - late final GeneratedColumn createdAtServer = GeneratedColumn( - 'created_at_server', + late final GeneratedColumn pendingDelete = GeneratedColumn( + 'pending_delete', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pending_delete" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _updatedAtServerMeta = const VerificationMeta( - 'updatedAtServer', + static const VerificationMeta _lastSyncedAtUtcMeta = const VerificationMeta( + 'lastSyncedAtUtc', ); @override - late final GeneratedColumn updatedAtServer = GeneratedColumn( - 'updated_at_server', + late final GeneratedColumn lastSyncedAtUtc = GeneratedColumn( + 'last_synced_at_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( - 'createdAtLocal', + static const VerificationMeta _createdLocalAtUtcMeta = const VerificationMeta( + 'createdLocalAtUtc', ); @override - late final GeneratedColumn createdAtLocal = GeneratedColumn( - 'created_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( - 'updatedAtLocal', + late final GeneratedColumn createdLocalAtUtc = + GeneratedColumn( + 'created_local_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedLocalAtUtcMeta = const VerificationMeta( + 'updatedLocalAtUtc', ); @override - late final GeneratedColumn updatedAtLocal = GeneratedColumn( - 'updated_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _syncStatusMeta = const VerificationMeta( - 'syncStatus', - ); - @override - late final GeneratedColumn syncStatus = GeneratedColumn( - 'sync_status', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('synced'), - ); - static const VerificationMeta _baselineRawJsonMeta = const VerificationMeta( - 'baselineRawJson', - ); - @override - late final GeneratedColumn baselineRawJson = GeneratedColumn( - 'baseline_raw_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); + late final GeneratedColumn updatedLocalAtUtc = + GeneratedColumn( + 'updated_local_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [ - id, accountId, - calendarSourceId, - provider, - providerCalendarId, - providerEventId, - providerRecurringEventId, - providerOriginalStartKey, - etagOrChangeKey, - status, + id, + davCollectionId, + kind, + etag, title, - description, - location, - allDay, - startDate, - startDateTime, - startTimeZone, - endDate, - endDateTime, - endTimeZone, - recurrenceJson, - remindersJson, - attendeesJson, - categoriesJson, - organizerJson, - creatorJson, - colorId, - colorHex, - visibility, - transparencyOrShowAs, - eventType, - webLink, - conferenceJson, - attachmentsJson, - isCancelled, - isDeleted, + updatedUtc, + selfLink, rawJson, - createdAtServer, - updatedAtServer, - createdAtLocal, - updatedAtLocal, - syncStatus, - baselineRawJson, + providerListKind, + isOwner, + isShared, + deltaLink, + providerMetadataJson, + serverMissing, + localDirty, + pendingDelete, + lastSyncedAtUtc, + createdLocalAtUtc, + updatedLocalAtUtc, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'calendar_events'; + static const String $name = 'task_lists'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } if (data.containsKey('account_id')) { context.handle( _accountIdMeta, @@ -8510,1662 +7091,778 @@ class $CalendarEventsTable extends CalendarEvents } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('calendar_source_id')) { - context.handle( - _calendarSourceIdMeta, - calendarSourceId.isAcceptableOrUnknown( - data['calendar_source_id']!, - _calendarSourceIdMeta, - ), - ); - } else if (isInserting) { - context.missing(_calendarSourceIdMeta); - } - if (data.containsKey('provider')) { - context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), - ); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } else if (isInserting) { - context.missing(_providerMeta); + context.missing(_idMeta); } - if (data.containsKey('provider_calendar_id')) { + if (data.containsKey('dav_collection_id')) { context.handle( - _providerCalendarIdMeta, - providerCalendarId.isAcceptableOrUnknown( - data['provider_calendar_id']!, - _providerCalendarIdMeta, + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, ), ); - } else if (isInserting) { - context.missing(_providerCalendarIdMeta); } - if (data.containsKey('provider_event_id')) { + if (data.containsKey('kind')) { context.handle( - _providerEventIdMeta, - providerEventId.isAcceptableOrUnknown( - data['provider_event_id']!, - _providerEventIdMeta, - ), + _kindMeta, + kind.isAcceptableOrUnknown(data['kind']!, _kindMeta), ); - } else if (isInserting) { - context.missing(_providerEventIdMeta); } - if (data.containsKey('provider_recurring_event_id')) { + if (data.containsKey('etag')) { context.handle( - _providerRecurringEventIdMeta, - providerRecurringEventId.isAcceptableOrUnknown( - data['provider_recurring_event_id']!, - _providerRecurringEventIdMeta, - ), + _etagMeta, + etag.isAcceptableOrUnknown(data['etag']!, _etagMeta), ); } - if (data.containsKey('provider_original_start_key')) { + if (data.containsKey('title')) { context.handle( - _providerOriginalStartKeyMeta, - providerOriginalStartKey.isAcceptableOrUnknown( - data['provider_original_start_key']!, - _providerOriginalStartKeyMeta, - ), + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), ); + } else if (isInserting) { + context.missing(_titleMeta); } - if (data.containsKey('etag_or_change_key')) { + if (data.containsKey('updated_utc')) { context.handle( - _etagOrChangeKeyMeta, - etagOrChangeKey.isAcceptableOrUnknown( - data['etag_or_change_key']!, - _etagOrChangeKeyMeta, - ), + _updatedUtcMeta, + updatedUtc.isAcceptableOrUnknown(data['updated_utc']!, _updatedUtcMeta), ); } - if (data.containsKey('status')) { + if (data.containsKey('self_link')) { context.handle( - _statusMeta, - status.isAcceptableOrUnknown(data['status']!, _statusMeta), + _selfLinkMeta, + selfLink.isAcceptableOrUnknown(data['self_link']!, _selfLinkMeta), ); } - if (data.containsKey('title')) { + if (data.containsKey('raw_json')) { context.handle( - _titleMeta, - title.isAcceptableOrUnknown(data['title']!, _titleMeta), + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), ); } else if (isInserting) { - context.missing(_titleMeta); + context.missing(_rawJsonMeta); } - if (data.containsKey('description')) { + if (data.containsKey('provider_list_kind')) { context.handle( - _descriptionMeta, - description.isAcceptableOrUnknown( - data['description']!, - _descriptionMeta, + _providerListKindMeta, + providerListKind.isAcceptableOrUnknown( + data['provider_list_kind']!, + _providerListKindMeta, ), ); } - if (data.containsKey('location')) { + if (data.containsKey('is_owner')) { context.handle( - _locationMeta, - location.isAcceptableOrUnknown(data['location']!, _locationMeta), + _isOwnerMeta, + isOwner.isAcceptableOrUnknown(data['is_owner']!, _isOwnerMeta), ); } - if (data.containsKey('all_day')) { + if (data.containsKey('is_shared')) { context.handle( - _allDayMeta, - allDay.isAcceptableOrUnknown(data['all_day']!, _allDayMeta), + _isSharedMeta, + isShared.isAcceptableOrUnknown(data['is_shared']!, _isSharedMeta), ); } - if (data.containsKey('start_date')) { + if (data.containsKey('delta_link')) { context.handle( - _startDateMeta, - startDate.isAcceptableOrUnknown(data['start_date']!, _startDateMeta), + _deltaLinkMeta, + deltaLink.isAcceptableOrUnknown(data['delta_link']!, _deltaLinkMeta), ); } - if (data.containsKey('start_date_time')) { + if (data.containsKey('provider_metadata_json')) { context.handle( - _startDateTimeMeta, - startDateTime.isAcceptableOrUnknown( - data['start_date_time']!, - _startDateTimeMeta, + _providerMetadataJsonMeta, + providerMetadataJson.isAcceptableOrUnknown( + data['provider_metadata_json']!, + _providerMetadataJsonMeta, ), ); } - if (data.containsKey('start_time_zone')) { + if (data.containsKey('server_missing')) { context.handle( - _startTimeZoneMeta, - startTimeZone.isAcceptableOrUnknown( - data['start_time_zone']!, - _startTimeZoneMeta, + _serverMissingMeta, + serverMissing.isAcceptableOrUnknown( + data['server_missing']!, + _serverMissingMeta, ), ); } - if (data.containsKey('end_date')) { + if (data.containsKey('local_dirty')) { context.handle( - _endDateMeta, - endDate.isAcceptableOrUnknown(data['end_date']!, _endDateMeta), + _localDirtyMeta, + localDirty.isAcceptableOrUnknown(data['local_dirty']!, _localDirtyMeta), ); } - if (data.containsKey('end_date_time')) { + if (data.containsKey('pending_delete')) { context.handle( - _endDateTimeMeta, - endDateTime.isAcceptableOrUnknown( - data['end_date_time']!, - _endDateTimeMeta, + _pendingDeleteMeta, + pendingDelete.isAcceptableOrUnknown( + data['pending_delete']!, + _pendingDeleteMeta, ), ); } - if (data.containsKey('end_time_zone')) { + if (data.containsKey('last_synced_at_utc')) { context.handle( - _endTimeZoneMeta, - endTimeZone.isAcceptableOrUnknown( - data['end_time_zone']!, - _endTimeZoneMeta, + _lastSyncedAtUtcMeta, + lastSyncedAtUtc.isAcceptableOrUnknown( + data['last_synced_at_utc']!, + _lastSyncedAtUtcMeta, ), ); } - if (data.containsKey('recurrence_json')) { + if (data.containsKey('created_local_at_utc')) { context.handle( - _recurrenceJsonMeta, - recurrenceJson.isAcceptableOrUnknown( - data['recurrence_json']!, - _recurrenceJsonMeta, + _createdLocalAtUtcMeta, + createdLocalAtUtc.isAcceptableOrUnknown( + data['created_local_at_utc']!, + _createdLocalAtUtcMeta, ), ); + } else if (isInserting) { + context.missing(_createdLocalAtUtcMeta); } - if (data.containsKey('reminders_json')) { + if (data.containsKey('updated_local_at_utc')) { context.handle( - _remindersJsonMeta, - remindersJson.isAcceptableOrUnknown( - data['reminders_json']!, - _remindersJsonMeta, - ), - ); - } - if (data.containsKey('attendees_json')) { - context.handle( - _attendeesJsonMeta, - attendeesJson.isAcceptableOrUnknown( - data['attendees_json']!, - _attendeesJsonMeta, - ), - ); - } - if (data.containsKey('categories_json')) { - context.handle( - _categoriesJsonMeta, - categoriesJson.isAcceptableOrUnknown( - data['categories_json']!, - _categoriesJsonMeta, - ), - ); - } - if (data.containsKey('organizer_json')) { - context.handle( - _organizerJsonMeta, - organizerJson.isAcceptableOrUnknown( - data['organizer_json']!, - _organizerJsonMeta, - ), - ); - } - if (data.containsKey('creator_json')) { - context.handle( - _creatorJsonMeta, - creatorJson.isAcceptableOrUnknown( - data['creator_json']!, - _creatorJsonMeta, - ), - ); - } - if (data.containsKey('color_id')) { - context.handle( - _colorIdMeta, - colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), - ); - } - if (data.containsKey('color_hex')) { - context.handle( - _colorHexMeta, - colorHex.isAcceptableOrUnknown(data['color_hex']!, _colorHexMeta), - ); - } - if (data.containsKey('visibility')) { - context.handle( - _visibilityMeta, - visibility.isAcceptableOrUnknown(data['visibility']!, _visibilityMeta), - ); - } - if (data.containsKey('transparency_or_show_as')) { - context.handle( - _transparencyOrShowAsMeta, - transparencyOrShowAs.isAcceptableOrUnknown( - data['transparency_or_show_as']!, - _transparencyOrShowAsMeta, - ), - ); - } - if (data.containsKey('event_type')) { - context.handle( - _eventTypeMeta, - eventType.isAcceptableOrUnknown(data['event_type']!, _eventTypeMeta), - ); - } - if (data.containsKey('web_link')) { - context.handle( - _webLinkMeta, - webLink.isAcceptableOrUnknown(data['web_link']!, _webLinkMeta), - ); - } - if (data.containsKey('conference_json')) { - context.handle( - _conferenceJsonMeta, - conferenceJson.isAcceptableOrUnknown( - data['conference_json']!, - _conferenceJsonMeta, - ), - ); - } - if (data.containsKey('attachments_json')) { - context.handle( - _attachmentsJsonMeta, - attachmentsJson.isAcceptableOrUnknown( - data['attachments_json']!, - _attachmentsJsonMeta, - ), - ); - } - if (data.containsKey('is_cancelled')) { - context.handle( - _isCancelledMeta, - isCancelled.isAcceptableOrUnknown( - data['is_cancelled']!, - _isCancelledMeta, - ), - ); - } - if (data.containsKey('is_deleted')) { - context.handle( - _isDeletedMeta, - isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta), - ); - } - if (data.containsKey('raw_json')) { - context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), - ); - } - if (data.containsKey('created_at_server')) { - context.handle( - _createdAtServerMeta, - createdAtServer.isAcceptableOrUnknown( - data['created_at_server']!, - _createdAtServerMeta, - ), - ); - } - if (data.containsKey('updated_at_server')) { - context.handle( - _updatedAtServerMeta, - updatedAtServer.isAcceptableOrUnknown( - data['updated_at_server']!, - _updatedAtServerMeta, - ), - ); - } - if (data.containsKey('created_at_local')) { - context.handle( - _createdAtLocalMeta, - createdAtLocal.isAcceptableOrUnknown( - data['created_at_local']!, - _createdAtLocalMeta, - ), - ); - } else if (isInserting) { - context.missing(_createdAtLocalMeta); - } - if (data.containsKey('updated_at_local')) { - context.handle( - _updatedAtLocalMeta, - updatedAtLocal.isAcceptableOrUnknown( - data['updated_at_local']!, - _updatedAtLocalMeta, + _updatedLocalAtUtcMeta, + updatedLocalAtUtc.isAcceptableOrUnknown( + data['updated_local_at_utc']!, + _updatedLocalAtUtcMeta, ), ); } else if (isInserting) { - context.missing(_updatedAtLocalMeta); - } - if (data.containsKey('sync_status')) { - context.handle( - _syncStatusMeta, - syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), - ); - } - if (data.containsKey('baseline_raw_json')) { - context.handle( - _baselineRawJsonMeta, - baselineRawJson.isAcceptableOrUnknown( - data['baseline_raw_json']!, - _baselineRawJsonMeta, - ), - ); + context.missing(_updatedLocalAtUtcMeta); } return context; } @override - Set get $primaryKey => {id}; + Set get $primaryKey => {accountId, id}; @override - CalendarEvent map(Map data, {String? tablePrefix}) { + TaskList map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarEvent( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, + return TaskList( accountId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - calendarSourceId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}calendar_source_id'], - )!, - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - )!, - providerCalendarId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider_calendar_id'], - )!, - providerEventId: attachedDatabase.typeMapping.read( + id: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_event_id'], + data['${effectivePrefix}id'], )!, - providerRecurringEventId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider_recurring_event_id'], - ), - providerOriginalStartKey: attachedDatabase.typeMapping.read( + davCollectionId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}provider_original_start_key'], + data['${effectivePrefix}dav_collection_id'], ), - etagOrChangeKey: attachedDatabase.typeMapping.read( + kind: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}etag_or_change_key'], + data['${effectivePrefix}kind'], ), - status: attachedDatabase.typeMapping.read( + etag: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}status'], + data['${effectivePrefix}etag'], ), title: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}title'], )!, - description: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}description'], - ), - location: attachedDatabase.typeMapping.read( + updatedUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}location'], + data['${effectivePrefix}updated_utc'], ), - allDay: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}all_day'], - )!, - startDate: attachedDatabase.typeMapping.read( + selfLink: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}start_date'], + data['${effectivePrefix}self_link'], ), - startDateTime: attachedDatabase.typeMapping.read( + rawJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}start_date_time'], - ), - startTimeZone: attachedDatabase.typeMapping.read( + data['${effectivePrefix}raw_json'], + )!, + providerListKind: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}start_time_zone'], + data['${effectivePrefix}provider_list_kind'], ), - endDate: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}end_date'], + isOwner: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_owner'], ), - endDateTime: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}end_date_time'], + isShared: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_shared'], ), - endTimeZone: attachedDatabase.typeMapping.read( + deltaLink: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}end_time_zone'], + data['${effectivePrefix}delta_link'], ), - recurrenceJson: attachedDatabase.typeMapping.read( + providerMetadataJson: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}recurrence_json'], + data['${effectivePrefix}provider_metadata_json'], ), - remindersJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}reminders_json'], - ), - attendeesJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}attendees_json'], - ), - categoriesJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}categories_json'], - ), - organizerJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}organizer_json'], - ), - creatorJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}creator_json'], - ), - colorId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}color_id'], - ), - colorHex: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}color_hex'], - ), - visibility: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}visibility'], - ), - transparencyOrShowAs: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}transparency_or_show_as'], - ), - eventType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}event_type'], - ), - webLink: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}web_link'], - ), - conferenceJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}conference_json'], - ), - attachmentsJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}attachments_json'], - ), - isCancelled: attachedDatabase.typeMapping.read( + serverMissing: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}is_cancelled'], + data['${effectivePrefix}server_missing'], )!, - isDeleted: attachedDatabase.typeMapping.read( + localDirty: attachedDatabase.typeMapping.read( DriftSqlType.bool, - data['${effectivePrefix}is_deleted'], + data['${effectivePrefix}local_dirty'], )!, - rawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_json'], - ), - createdAtServer: attachedDatabase.typeMapping.read( + pendingDelete: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pending_delete'], + )!, + lastSyncedAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}created_at_server'], + data['${effectivePrefix}last_synced_at_utc'], ), - updatedAtServer: attachedDatabase.typeMapping.read( + createdLocalAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}updated_at_server'], - ), - createdAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}created_at_local'], - )!, - updatedAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}updated_at_local'], + data['${effectivePrefix}created_local_at_utc'], )!, - syncStatus: attachedDatabase.typeMapping.read( + updatedLocalAtUtc: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}sync_status'], + data['${effectivePrefix}updated_local_at_utc'], )!, - baselineRawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}baseline_raw_json'], - ), ); } @override - $CalendarEventsTable createAlias(String alias) { - return $CalendarEventsTable(attachedDatabase, alias); + $TaskListsTable createAlias(String alias) { + return $TaskListsTable(attachedDatabase, alias); } } -class CalendarEvent extends DataClass implements Insertable { - final String id; +class TaskList extends DataClass implements Insertable { final String accountId; - final String calendarSourceId; - final String provider; - final String providerCalendarId; - final String providerEventId; - final String? providerRecurringEventId; - final String? providerOriginalStartKey; - final String? etagOrChangeKey; - final String? status; + final String id; + final String? davCollectionId; + final String? kind; + final String? etag; final String title; - final String? description; - final String? location; - final bool allDay; - final String? startDate; - final String? startDateTime; - final String? startTimeZone; - final String? endDate; - final String? endDateTime; - final String? endTimeZone; - final String? recurrenceJson; - final String? remindersJson; - final String? attendeesJson; - final String? categoriesJson; - final String? organizerJson; - final String? creatorJson; - final String? colorId; - final String? colorHex; - final String? visibility; - final String? transparencyOrShowAs; - final String? eventType; - final String? webLink; - final String? conferenceJson; - final String? attachmentsJson; - final bool isCancelled; - final bool isDeleted; - final String? rawJson; - final String? createdAtServer; - final String? updatedAtServer; - final int createdAtLocal; - final int updatedAtLocal; - final String syncStatus; - final String? baselineRawJson; - const CalendarEvent({ - required this.id, + final String? updatedUtc; + final String? selfLink; + final String rawJson; + final String? providerListKind; + final bool? isOwner; + final bool? isShared; + final String? deltaLink; + final String? providerMetadataJson; + final bool serverMissing; + final bool localDirty; + final bool pendingDelete; + final String? lastSyncedAtUtc; + final String createdLocalAtUtc; + final String updatedLocalAtUtc; + const TaskList({ required this.accountId, - required this.calendarSourceId, - required this.provider, - required this.providerCalendarId, - required this.providerEventId, - this.providerRecurringEventId, - this.providerOriginalStartKey, - this.etagOrChangeKey, - this.status, + required this.id, + this.davCollectionId, + this.kind, + this.etag, required this.title, - this.description, - this.location, - required this.allDay, - this.startDate, - this.startDateTime, - this.startTimeZone, - this.endDate, - this.endDateTime, - this.endTimeZone, - this.recurrenceJson, - this.remindersJson, - this.attendeesJson, - this.categoriesJson, - this.organizerJson, - this.creatorJson, - this.colorId, - this.colorHex, - this.visibility, - this.transparencyOrShowAs, - this.eventType, - this.webLink, - this.conferenceJson, - this.attachmentsJson, - required this.isCancelled, - required this.isDeleted, - this.rawJson, - this.createdAtServer, - this.updatedAtServer, - required this.createdAtLocal, - required this.updatedAtLocal, - required this.syncStatus, - this.baselineRawJson, + this.updatedUtc, + this.selfLink, + required this.rawJson, + this.providerListKind, + this.isOwner, + this.isShared, + this.deltaLink, + this.providerMetadataJson, + required this.serverMissing, + required this.localDirty, + required this.pendingDelete, + this.lastSyncedAtUtc, + required this.createdLocalAtUtc, + required this.updatedLocalAtUtc, }); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); map['account_id'] = Variable(accountId); - map['calendar_source_id'] = Variable(calendarSourceId); - map['provider'] = Variable(provider); - map['provider_calendar_id'] = Variable(providerCalendarId); - map['provider_event_id'] = Variable(providerEventId); - if (!nullToAbsent || providerRecurringEventId != null) { - map['provider_recurring_event_id'] = Variable( - providerRecurringEventId, - ); - } - if (!nullToAbsent || providerOriginalStartKey != null) { - map['provider_original_start_key'] = Variable( - providerOriginalStartKey, - ); + map['id'] = Variable(id); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); } - if (!nullToAbsent || etagOrChangeKey != null) { - map['etag_or_change_key'] = Variable(etagOrChangeKey); + if (!nullToAbsent || kind != null) { + map['kind'] = Variable(kind); } - if (!nullToAbsent || status != null) { - map['status'] = Variable(status); + if (!nullToAbsent || etag != null) { + map['etag'] = Variable(etag); } map['title'] = Variable(title); - if (!nullToAbsent || description != null) { - map['description'] = Variable(description); + if (!nullToAbsent || updatedUtc != null) { + map['updated_utc'] = Variable(updatedUtc); } - if (!nullToAbsent || location != null) { - map['location'] = Variable(location); + if (!nullToAbsent || selfLink != null) { + map['self_link'] = Variable(selfLink); } - map['all_day'] = Variable(allDay); - if (!nullToAbsent || startDate != null) { - map['start_date'] = Variable(startDate); + map['raw_json'] = Variable(rawJson); + if (!nullToAbsent || providerListKind != null) { + map['provider_list_kind'] = Variable(providerListKind); } - if (!nullToAbsent || startDateTime != null) { - map['start_date_time'] = Variable(startDateTime); + if (!nullToAbsent || isOwner != null) { + map['is_owner'] = Variable(isOwner); } - if (!nullToAbsent || startTimeZone != null) { - map['start_time_zone'] = Variable(startTimeZone); - } - if (!nullToAbsent || endDate != null) { - map['end_date'] = Variable(endDate); - } - if (!nullToAbsent || endDateTime != null) { - map['end_date_time'] = Variable(endDateTime); - } - if (!nullToAbsent || endTimeZone != null) { - map['end_time_zone'] = Variable(endTimeZone); - } - if (!nullToAbsent || recurrenceJson != null) { - map['recurrence_json'] = Variable(recurrenceJson); - } - if (!nullToAbsent || remindersJson != null) { - map['reminders_json'] = Variable(remindersJson); - } - if (!nullToAbsent || attendeesJson != null) { - map['attendees_json'] = Variable(attendeesJson); - } - if (!nullToAbsent || categoriesJson != null) { - map['categories_json'] = Variable(categoriesJson); - } - if (!nullToAbsent || organizerJson != null) { - map['organizer_json'] = Variable(organizerJson); - } - if (!nullToAbsent || creatorJson != null) { - map['creator_json'] = Variable(creatorJson); - } - if (!nullToAbsent || colorId != null) { - map['color_id'] = Variable(colorId); - } - if (!nullToAbsent || colorHex != null) { - map['color_hex'] = Variable(colorHex); - } - if (!nullToAbsent || visibility != null) { - map['visibility'] = Variable(visibility); - } - if (!nullToAbsent || transparencyOrShowAs != null) { - map['transparency_or_show_as'] = Variable(transparencyOrShowAs); - } - if (!nullToAbsent || eventType != null) { - map['event_type'] = Variable(eventType); - } - if (!nullToAbsent || webLink != null) { - map['web_link'] = Variable(webLink); - } - if (!nullToAbsent || conferenceJson != null) { - map['conference_json'] = Variable(conferenceJson); - } - if (!nullToAbsent || attachmentsJson != null) { - map['attachments_json'] = Variable(attachmentsJson); - } - map['is_cancelled'] = Variable(isCancelled); - map['is_deleted'] = Variable(isDeleted); - if (!nullToAbsent || rawJson != null) { - map['raw_json'] = Variable(rawJson); + if (!nullToAbsent || isShared != null) { + map['is_shared'] = Variable(isShared); } - if (!nullToAbsent || createdAtServer != null) { - map['created_at_server'] = Variable(createdAtServer); + if (!nullToAbsent || deltaLink != null) { + map['delta_link'] = Variable(deltaLink); } - if (!nullToAbsent || updatedAtServer != null) { - map['updated_at_server'] = Variable(updatedAtServer); + if (!nullToAbsent || providerMetadataJson != null) { + map['provider_metadata_json'] = Variable(providerMetadataJson); } - map['created_at_local'] = Variable(createdAtLocal); - map['updated_at_local'] = Variable(updatedAtLocal); - map['sync_status'] = Variable(syncStatus); - if (!nullToAbsent || baselineRawJson != null) { - map['baseline_raw_json'] = Variable(baselineRawJson); + map['server_missing'] = Variable(serverMissing); + map['local_dirty'] = Variable(localDirty); + map['pending_delete'] = Variable(pendingDelete); + if (!nullToAbsent || lastSyncedAtUtc != null) { + map['last_synced_at_utc'] = Variable(lastSyncedAtUtc); } + map['created_local_at_utc'] = Variable(createdLocalAtUtc); + map['updated_local_at_utc'] = Variable(updatedLocalAtUtc); return map; } - CalendarEventsCompanion toCompanion(bool nullToAbsent) { - return CalendarEventsCompanion( - id: Value(id), + TaskListsCompanion toCompanion(bool nullToAbsent) { + return TaskListsCompanion( accountId: Value(accountId), - calendarSourceId: Value(calendarSourceId), - provider: Value(provider), - providerCalendarId: Value(providerCalendarId), - providerEventId: Value(providerEventId), - providerRecurringEventId: providerRecurringEventId == null && nullToAbsent - ? const Value.absent() - : Value(providerRecurringEventId), - providerOriginalStartKey: providerOriginalStartKey == null && nullToAbsent - ? const Value.absent() - : Value(providerOriginalStartKey), - etagOrChangeKey: etagOrChangeKey == null && nullToAbsent - ? const Value.absent() - : Value(etagOrChangeKey), - status: status == null && nullToAbsent + id: Value(id), + davCollectionId: davCollectionId == null && nullToAbsent ? const Value.absent() - : Value(status), + : Value(davCollectionId), + kind: kind == null && nullToAbsent ? const Value.absent() : Value(kind), + etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag), title: Value(title), - description: description == null && nullToAbsent - ? const Value.absent() - : Value(description), - location: location == null && nullToAbsent - ? const Value.absent() - : Value(location), - allDay: Value(allDay), - startDate: startDate == null && nullToAbsent - ? const Value.absent() - : Value(startDate), - startDateTime: startDateTime == null && nullToAbsent - ? const Value.absent() - : Value(startDateTime), - startTimeZone: startTimeZone == null && nullToAbsent - ? const Value.absent() - : Value(startTimeZone), - endDate: endDate == null && nullToAbsent - ? const Value.absent() - : Value(endDate), - endDateTime: endDateTime == null && nullToAbsent - ? const Value.absent() - : Value(endDateTime), - endTimeZone: endTimeZone == null && nullToAbsent - ? const Value.absent() - : Value(endTimeZone), - recurrenceJson: recurrenceJson == null && nullToAbsent - ? const Value.absent() - : Value(recurrenceJson), - remindersJson: remindersJson == null && nullToAbsent - ? const Value.absent() - : Value(remindersJson), - attendeesJson: attendeesJson == null && nullToAbsent - ? const Value.absent() - : Value(attendeesJson), - categoriesJson: categoriesJson == null && nullToAbsent - ? const Value.absent() - : Value(categoriesJson), - organizerJson: organizerJson == null && nullToAbsent - ? const Value.absent() - : Value(organizerJson), - creatorJson: creatorJson == null && nullToAbsent - ? const Value.absent() - : Value(creatorJson), - colorId: colorId == null && nullToAbsent - ? const Value.absent() - : Value(colorId), - colorHex: colorHex == null && nullToAbsent - ? const Value.absent() - : Value(colorHex), - visibility: visibility == null && nullToAbsent - ? const Value.absent() - : Value(visibility), - transparencyOrShowAs: transparencyOrShowAs == null && nullToAbsent - ? const Value.absent() - : Value(transparencyOrShowAs), - eventType: eventType == null && nullToAbsent + updatedUtc: updatedUtc == null && nullToAbsent ? const Value.absent() - : Value(eventType), - webLink: webLink == null && nullToAbsent + : Value(updatedUtc), + selfLink: selfLink == null && nullToAbsent ? const Value.absent() - : Value(webLink), - conferenceJson: conferenceJson == null && nullToAbsent + : Value(selfLink), + rawJson: Value(rawJson), + providerListKind: providerListKind == null && nullToAbsent ? const Value.absent() - : Value(conferenceJson), - attachmentsJson: attachmentsJson == null && nullToAbsent + : Value(providerListKind), + isOwner: isOwner == null && nullToAbsent ? const Value.absent() - : Value(attachmentsJson), - isCancelled: Value(isCancelled), - isDeleted: Value(isDeleted), - rawJson: rawJson == null && nullToAbsent + : Value(isOwner), + isShared: isShared == null && nullToAbsent ? const Value.absent() - : Value(rawJson), - createdAtServer: createdAtServer == null && nullToAbsent + : Value(isShared), + deltaLink: deltaLink == null && nullToAbsent ? const Value.absent() - : Value(createdAtServer), - updatedAtServer: updatedAtServer == null && nullToAbsent + : Value(deltaLink), + providerMetadataJson: providerMetadataJson == null && nullToAbsent ? const Value.absent() - : Value(updatedAtServer), - createdAtLocal: Value(createdAtLocal), - updatedAtLocal: Value(updatedAtLocal), - syncStatus: Value(syncStatus), - baselineRawJson: baselineRawJson == null && nullToAbsent + : Value(providerMetadataJson), + serverMissing: Value(serverMissing), + localDirty: Value(localDirty), + pendingDelete: Value(pendingDelete), + lastSyncedAtUtc: lastSyncedAtUtc == null && nullToAbsent ? const Value.absent() - : Value(baselineRawJson), + : Value(lastSyncedAtUtc), + createdLocalAtUtc: Value(createdLocalAtUtc), + updatedLocalAtUtc: Value(updatedLocalAtUtc), ); } - factory CalendarEvent.fromJson( + factory TaskList.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarEvent( - id: serializer.fromJson(json['id']), + return TaskList( accountId: serializer.fromJson(json['accountId']), - calendarSourceId: serializer.fromJson(json['calendarSourceId']), - provider: serializer.fromJson(json['provider']), - providerCalendarId: serializer.fromJson( - json['providerCalendarId'], - ), - providerEventId: serializer.fromJson(json['providerEventId']), - providerRecurringEventId: serializer.fromJson( - json['providerRecurringEventId'], - ), - providerOriginalStartKey: serializer.fromJson( - json['providerOriginalStartKey'], - ), - etagOrChangeKey: serializer.fromJson(json['etagOrChangeKey']), - status: serializer.fromJson(json['status']), + id: serializer.fromJson(json['id']), + davCollectionId: serializer.fromJson(json['davCollectionId']), + kind: serializer.fromJson(json['kind']), + etag: serializer.fromJson(json['etag']), title: serializer.fromJson(json['title']), - description: serializer.fromJson(json['description']), - location: serializer.fromJson(json['location']), - allDay: serializer.fromJson(json['allDay']), - startDate: serializer.fromJson(json['startDate']), - startDateTime: serializer.fromJson(json['startDateTime']), - startTimeZone: serializer.fromJson(json['startTimeZone']), - endDate: serializer.fromJson(json['endDate']), - endDateTime: serializer.fromJson(json['endDateTime']), - endTimeZone: serializer.fromJson(json['endTimeZone']), - recurrenceJson: serializer.fromJson(json['recurrenceJson']), - remindersJson: serializer.fromJson(json['remindersJson']), - attendeesJson: serializer.fromJson(json['attendeesJson']), - categoriesJson: serializer.fromJson(json['categoriesJson']), - organizerJson: serializer.fromJson(json['organizerJson']), - creatorJson: serializer.fromJson(json['creatorJson']), - colorId: serializer.fromJson(json['colorId']), - colorHex: serializer.fromJson(json['colorHex']), - visibility: serializer.fromJson(json['visibility']), - transparencyOrShowAs: serializer.fromJson( - json['transparencyOrShowAs'], + updatedUtc: serializer.fromJson(json['updatedUtc']), + selfLink: serializer.fromJson(json['selfLink']), + rawJson: serializer.fromJson(json['rawJson']), + providerListKind: serializer.fromJson(json['providerListKind']), + isOwner: serializer.fromJson(json['isOwner']), + isShared: serializer.fromJson(json['isShared']), + deltaLink: serializer.fromJson(json['deltaLink']), + providerMetadataJson: serializer.fromJson( + json['providerMetadataJson'], ), - eventType: serializer.fromJson(json['eventType']), - webLink: serializer.fromJson(json['webLink']), - conferenceJson: serializer.fromJson(json['conferenceJson']), - attachmentsJson: serializer.fromJson(json['attachmentsJson']), - isCancelled: serializer.fromJson(json['isCancelled']), - isDeleted: serializer.fromJson(json['isDeleted']), - rawJson: serializer.fromJson(json['rawJson']), - createdAtServer: serializer.fromJson(json['createdAtServer']), - updatedAtServer: serializer.fromJson(json['updatedAtServer']), - createdAtLocal: serializer.fromJson(json['createdAtLocal']), - updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), - syncStatus: serializer.fromJson(json['syncStatus']), - baselineRawJson: serializer.fromJson(json['baselineRawJson']), + serverMissing: serializer.fromJson(json['serverMissing']), + localDirty: serializer.fromJson(json['localDirty']), + pendingDelete: serializer.fromJson(json['pendingDelete']), + lastSyncedAtUtc: serializer.fromJson(json['lastSyncedAtUtc']), + createdLocalAtUtc: serializer.fromJson(json['createdLocalAtUtc']), + updatedLocalAtUtc: serializer.fromJson(json['updatedLocalAtUtc']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), 'accountId': serializer.toJson(accountId), - 'calendarSourceId': serializer.toJson(calendarSourceId), - 'provider': serializer.toJson(provider), - 'providerCalendarId': serializer.toJson(providerCalendarId), - 'providerEventId': serializer.toJson(providerEventId), - 'providerRecurringEventId': serializer.toJson( - providerRecurringEventId, - ), - 'providerOriginalStartKey': serializer.toJson( - providerOriginalStartKey, - ), - 'etagOrChangeKey': serializer.toJson(etagOrChangeKey), - 'status': serializer.toJson(status), + 'id': serializer.toJson(id), + 'davCollectionId': serializer.toJson(davCollectionId), + 'kind': serializer.toJson(kind), + 'etag': serializer.toJson(etag), 'title': serializer.toJson(title), - 'description': serializer.toJson(description), - 'location': serializer.toJson(location), - 'allDay': serializer.toJson(allDay), - 'startDate': serializer.toJson(startDate), - 'startDateTime': serializer.toJson(startDateTime), - 'startTimeZone': serializer.toJson(startTimeZone), - 'endDate': serializer.toJson(endDate), - 'endDateTime': serializer.toJson(endDateTime), - 'endTimeZone': serializer.toJson(endTimeZone), - 'recurrenceJson': serializer.toJson(recurrenceJson), - 'remindersJson': serializer.toJson(remindersJson), - 'attendeesJson': serializer.toJson(attendeesJson), - 'categoriesJson': serializer.toJson(categoriesJson), - 'organizerJson': serializer.toJson(organizerJson), - 'creatorJson': serializer.toJson(creatorJson), - 'colorId': serializer.toJson(colorId), - 'colorHex': serializer.toJson(colorHex), - 'visibility': serializer.toJson(visibility), - 'transparencyOrShowAs': serializer.toJson(transparencyOrShowAs), - 'eventType': serializer.toJson(eventType), - 'webLink': serializer.toJson(webLink), - 'conferenceJson': serializer.toJson(conferenceJson), - 'attachmentsJson': serializer.toJson(attachmentsJson), - 'isCancelled': serializer.toJson(isCancelled), - 'isDeleted': serializer.toJson(isDeleted), - 'rawJson': serializer.toJson(rawJson), - 'createdAtServer': serializer.toJson(createdAtServer), - 'updatedAtServer': serializer.toJson(updatedAtServer), - 'createdAtLocal': serializer.toJson(createdAtLocal), - 'updatedAtLocal': serializer.toJson(updatedAtLocal), - 'syncStatus': serializer.toJson(syncStatus), - 'baselineRawJson': serializer.toJson(baselineRawJson), + 'updatedUtc': serializer.toJson(updatedUtc), + 'selfLink': serializer.toJson(selfLink), + 'rawJson': serializer.toJson(rawJson), + 'providerListKind': serializer.toJson(providerListKind), + 'isOwner': serializer.toJson(isOwner), + 'isShared': serializer.toJson(isShared), + 'deltaLink': serializer.toJson(deltaLink), + 'providerMetadataJson': serializer.toJson(providerMetadataJson), + 'serverMissing': serializer.toJson(serverMissing), + 'localDirty': serializer.toJson(localDirty), + 'pendingDelete': serializer.toJson(pendingDelete), + 'lastSyncedAtUtc': serializer.toJson(lastSyncedAtUtc), + 'createdLocalAtUtc': serializer.toJson(createdLocalAtUtc), + 'updatedLocalAtUtc': serializer.toJson(updatedLocalAtUtc), }; } - CalendarEvent copyWith({ - String? id, + TaskList copyWith({ String? accountId, - String? calendarSourceId, - String? provider, - String? providerCalendarId, - String? providerEventId, - Value providerRecurringEventId = const Value.absent(), - Value providerOriginalStartKey = const Value.absent(), - Value etagOrChangeKey = const Value.absent(), - Value status = const Value.absent(), + String? id, + Value davCollectionId = const Value.absent(), + Value kind = const Value.absent(), + Value etag = const Value.absent(), String? title, - Value description = const Value.absent(), - Value location = const Value.absent(), - bool? allDay, - Value startDate = const Value.absent(), - Value startDateTime = const Value.absent(), - Value startTimeZone = const Value.absent(), - Value endDate = const Value.absent(), - Value endDateTime = const Value.absent(), - Value endTimeZone = const Value.absent(), - Value recurrenceJson = const Value.absent(), - Value remindersJson = const Value.absent(), - Value attendeesJson = const Value.absent(), - Value categoriesJson = const Value.absent(), - Value organizerJson = const Value.absent(), - Value creatorJson = const Value.absent(), - Value colorId = const Value.absent(), - Value colorHex = const Value.absent(), - Value visibility = const Value.absent(), - Value transparencyOrShowAs = const Value.absent(), - Value eventType = const Value.absent(), - Value webLink = const Value.absent(), - Value conferenceJson = const Value.absent(), - Value attachmentsJson = const Value.absent(), - bool? isCancelled, - bool? isDeleted, - Value rawJson = const Value.absent(), - Value createdAtServer = const Value.absent(), - Value updatedAtServer = const Value.absent(), - int? createdAtLocal, - int? updatedAtLocal, - String? syncStatus, - Value baselineRawJson = const Value.absent(), - }) => CalendarEvent( - id: id ?? this.id, + Value updatedUtc = const Value.absent(), + Value selfLink = const Value.absent(), + String? rawJson, + Value providerListKind = const Value.absent(), + Value isOwner = const Value.absent(), + Value isShared = const Value.absent(), + Value deltaLink = const Value.absent(), + Value providerMetadataJson = const Value.absent(), + bool? serverMissing, + bool? localDirty, + bool? pendingDelete, + Value lastSyncedAtUtc = const Value.absent(), + String? createdLocalAtUtc, + String? updatedLocalAtUtc, + }) => TaskList( accountId: accountId ?? this.accountId, - calendarSourceId: calendarSourceId ?? this.calendarSourceId, - provider: provider ?? this.provider, - providerCalendarId: providerCalendarId ?? this.providerCalendarId, - providerEventId: providerEventId ?? this.providerEventId, - providerRecurringEventId: providerRecurringEventId.present - ? providerRecurringEventId.value - : this.providerRecurringEventId, - providerOriginalStartKey: providerOriginalStartKey.present - ? providerOriginalStartKey.value - : this.providerOriginalStartKey, - etagOrChangeKey: etagOrChangeKey.present - ? etagOrChangeKey.value - : this.etagOrChangeKey, - status: status.present ? status.value : this.status, + id: id ?? this.id, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + kind: kind.present ? kind.value : this.kind, + etag: etag.present ? etag.value : this.etag, title: title ?? this.title, - description: description.present ? description.value : this.description, - location: location.present ? location.value : this.location, - allDay: allDay ?? this.allDay, - startDate: startDate.present ? startDate.value : this.startDate, - startDateTime: startDateTime.present - ? startDateTime.value - : this.startDateTime, - startTimeZone: startTimeZone.present - ? startTimeZone.value - : this.startTimeZone, - endDate: endDate.present ? endDate.value : this.endDate, - endDateTime: endDateTime.present ? endDateTime.value : this.endDateTime, - endTimeZone: endTimeZone.present ? endTimeZone.value : this.endTimeZone, - recurrenceJson: recurrenceJson.present - ? recurrenceJson.value - : this.recurrenceJson, - remindersJson: remindersJson.present - ? remindersJson.value - : this.remindersJson, - attendeesJson: attendeesJson.present - ? attendeesJson.value - : this.attendeesJson, - categoriesJson: categoriesJson.present - ? categoriesJson.value - : this.categoriesJson, - organizerJson: organizerJson.present - ? organizerJson.value - : this.organizerJson, - creatorJson: creatorJson.present ? creatorJson.value : this.creatorJson, - colorId: colorId.present ? colorId.value : this.colorId, - colorHex: colorHex.present ? colorHex.value : this.colorHex, - visibility: visibility.present ? visibility.value : this.visibility, - transparencyOrShowAs: transparencyOrShowAs.present - ? transparencyOrShowAs.value - : this.transparencyOrShowAs, - eventType: eventType.present ? eventType.value : this.eventType, - webLink: webLink.present ? webLink.value : this.webLink, - conferenceJson: conferenceJson.present - ? conferenceJson.value - : this.conferenceJson, - attachmentsJson: attachmentsJson.present - ? attachmentsJson.value - : this.attachmentsJson, - isCancelled: isCancelled ?? this.isCancelled, - isDeleted: isDeleted ?? this.isDeleted, - rawJson: rawJson.present ? rawJson.value : this.rawJson, - createdAtServer: createdAtServer.present - ? createdAtServer.value - : this.createdAtServer, - updatedAtServer: updatedAtServer.present - ? updatedAtServer.value - : this.updatedAtServer, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - syncStatus: syncStatus ?? this.syncStatus, - baselineRawJson: baselineRawJson.present - ? baselineRawJson.value - : this.baselineRawJson, + updatedUtc: updatedUtc.present ? updatedUtc.value : this.updatedUtc, + selfLink: selfLink.present ? selfLink.value : this.selfLink, + rawJson: rawJson ?? this.rawJson, + providerListKind: providerListKind.present + ? providerListKind.value + : this.providerListKind, + isOwner: isOwner.present ? isOwner.value : this.isOwner, + isShared: isShared.present ? isShared.value : this.isShared, + deltaLink: deltaLink.present ? deltaLink.value : this.deltaLink, + providerMetadataJson: providerMetadataJson.present + ? providerMetadataJson.value + : this.providerMetadataJson, + serverMissing: serverMissing ?? this.serverMissing, + localDirty: localDirty ?? this.localDirty, + pendingDelete: pendingDelete ?? this.pendingDelete, + lastSyncedAtUtc: lastSyncedAtUtc.present + ? lastSyncedAtUtc.value + : this.lastSyncedAtUtc, + createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, + updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, ); - CalendarEvent copyWithCompanion(CalendarEventsCompanion data) { - return CalendarEvent( - id: data.id.present ? data.id.value : this.id, + TaskList copyWithCompanion(TaskListsCompanion data) { + return TaskList( accountId: data.accountId.present ? data.accountId.value : this.accountId, - calendarSourceId: data.calendarSourceId.present - ? data.calendarSourceId.value - : this.calendarSourceId, - provider: data.provider.present ? data.provider.value : this.provider, - providerCalendarId: data.providerCalendarId.present - ? data.providerCalendarId.value - : this.providerCalendarId, - providerEventId: data.providerEventId.present - ? data.providerEventId.value - : this.providerEventId, - providerRecurringEventId: data.providerRecurringEventId.present - ? data.providerRecurringEventId.value - : this.providerRecurringEventId, - providerOriginalStartKey: data.providerOriginalStartKey.present - ? data.providerOriginalStartKey.value - : this.providerOriginalStartKey, - etagOrChangeKey: data.etagOrChangeKey.present - ? data.etagOrChangeKey.value - : this.etagOrChangeKey, - status: data.status.present ? data.status.value : this.status, + id: data.id.present ? data.id.value : this.id, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + kind: data.kind.present ? data.kind.value : this.kind, + etag: data.etag.present ? data.etag.value : this.etag, title: data.title.present ? data.title.value : this.title, - description: data.description.present - ? data.description.value - : this.description, - location: data.location.present ? data.location.value : this.location, - allDay: data.allDay.present ? data.allDay.value : this.allDay, - startDate: data.startDate.present ? data.startDate.value : this.startDate, - startDateTime: data.startDateTime.present - ? data.startDateTime.value - : this.startDateTime, - startTimeZone: data.startTimeZone.present - ? data.startTimeZone.value - : this.startTimeZone, - endDate: data.endDate.present ? data.endDate.value : this.endDate, - endDateTime: data.endDateTime.present - ? data.endDateTime.value - : this.endDateTime, - endTimeZone: data.endTimeZone.present - ? data.endTimeZone.value - : this.endTimeZone, - recurrenceJson: data.recurrenceJson.present - ? data.recurrenceJson.value - : this.recurrenceJson, - remindersJson: data.remindersJson.present - ? data.remindersJson.value - : this.remindersJson, - attendeesJson: data.attendeesJson.present - ? data.attendeesJson.value - : this.attendeesJson, - categoriesJson: data.categoriesJson.present - ? data.categoriesJson.value - : this.categoriesJson, - organizerJson: data.organizerJson.present - ? data.organizerJson.value - : this.organizerJson, - creatorJson: data.creatorJson.present - ? data.creatorJson.value - : this.creatorJson, - colorId: data.colorId.present ? data.colorId.value : this.colorId, - colorHex: data.colorHex.present ? data.colorHex.value : this.colorHex, - visibility: data.visibility.present - ? data.visibility.value - : this.visibility, - transparencyOrShowAs: data.transparencyOrShowAs.present - ? data.transparencyOrShowAs.value - : this.transparencyOrShowAs, - eventType: data.eventType.present ? data.eventType.value : this.eventType, - webLink: data.webLink.present ? data.webLink.value : this.webLink, - conferenceJson: data.conferenceJson.present - ? data.conferenceJson.value - : this.conferenceJson, - attachmentsJson: data.attachmentsJson.present - ? data.attachmentsJson.value - : this.attachmentsJson, - isCancelled: data.isCancelled.present - ? data.isCancelled.value - : this.isCancelled, - isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, + updatedUtc: data.updatedUtc.present + ? data.updatedUtc.value + : this.updatedUtc, + selfLink: data.selfLink.present ? data.selfLink.value : this.selfLink, rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - createdAtServer: data.createdAtServer.present - ? data.createdAtServer.value - : this.createdAtServer, - updatedAtServer: data.updatedAtServer.present - ? data.updatedAtServer.value - : this.updatedAtServer, - createdAtLocal: data.createdAtLocal.present - ? data.createdAtLocal.value - : this.createdAtLocal, - updatedAtLocal: data.updatedAtLocal.present - ? data.updatedAtLocal.value - : this.updatedAtLocal, - syncStatus: data.syncStatus.present - ? data.syncStatus.value - : this.syncStatus, - baselineRawJson: data.baselineRawJson.present - ? data.baselineRawJson.value - : this.baselineRawJson, + providerListKind: data.providerListKind.present + ? data.providerListKind.value + : this.providerListKind, + isOwner: data.isOwner.present ? data.isOwner.value : this.isOwner, + isShared: data.isShared.present ? data.isShared.value : this.isShared, + deltaLink: data.deltaLink.present ? data.deltaLink.value : this.deltaLink, + providerMetadataJson: data.providerMetadataJson.present + ? data.providerMetadataJson.value + : this.providerMetadataJson, + serverMissing: data.serverMissing.present + ? data.serverMissing.value + : this.serverMissing, + localDirty: data.localDirty.present + ? data.localDirty.value + : this.localDirty, + pendingDelete: data.pendingDelete.present + ? data.pendingDelete.value + : this.pendingDelete, + lastSyncedAtUtc: data.lastSyncedAtUtc.present + ? data.lastSyncedAtUtc.value + : this.lastSyncedAtUtc, + createdLocalAtUtc: data.createdLocalAtUtc.present + ? data.createdLocalAtUtc.value + : this.createdLocalAtUtc, + updatedLocalAtUtc: data.updatedLocalAtUtc.present + ? data.updatedLocalAtUtc.value + : this.updatedLocalAtUtc, ); } @override String toString() { - return (StringBuffer('CalendarEvent(') - ..write('id: $id, ') + return (StringBuffer('TaskList(') ..write('accountId: $accountId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('provider: $provider, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('providerEventId: $providerEventId, ') - ..write('providerRecurringEventId: $providerRecurringEventId, ') - ..write('providerOriginalStartKey: $providerOriginalStartKey, ') - ..write('etagOrChangeKey: $etagOrChangeKey, ') - ..write('status: $status, ') + ..write('id: $id, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('kind: $kind, ') + ..write('etag: $etag, ') ..write('title: $title, ') - ..write('description: $description, ') - ..write('location: $location, ') - ..write('allDay: $allDay, ') - ..write('startDate: $startDate, ') - ..write('startDateTime: $startDateTime, ') - ..write('startTimeZone: $startTimeZone, ') - ..write('endDate: $endDate, ') - ..write('endDateTime: $endDateTime, ') - ..write('endTimeZone: $endTimeZone, ') - ..write('recurrenceJson: $recurrenceJson, ') - ..write('remindersJson: $remindersJson, ') - ..write('attendeesJson: $attendeesJson, ') - ..write('categoriesJson: $categoriesJson, ') - ..write('organizerJson: $organizerJson, ') - ..write('creatorJson: $creatorJson, ') - ..write('colorId: $colorId, ') - ..write('colorHex: $colorHex, ') - ..write('visibility: $visibility, ') - ..write('transparencyOrShowAs: $transparencyOrShowAs, ') - ..write('eventType: $eventType, ') - ..write('webLink: $webLink, ') - ..write('conferenceJson: $conferenceJson, ') - ..write('attachmentsJson: $attachmentsJson, ') - ..write('isCancelled: $isCancelled, ') - ..write('isDeleted: $isDeleted, ') + ..write('updatedUtc: $updatedUtc, ') + ..write('selfLink: $selfLink, ') ..write('rawJson: $rawJson, ') - ..write('createdAtServer: $createdAtServer, ') - ..write('updatedAtServer: $updatedAtServer, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal, ') - ..write('syncStatus: $syncStatus, ') - ..write('baselineRawJson: $baselineRawJson') + ..write('providerListKind: $providerListKind, ') + ..write('isOwner: $isOwner, ') + ..write('isShared: $isShared, ') + ..write('deltaLink: $deltaLink, ') + ..write('providerMetadataJson: $providerMetadataJson, ') + ..write('serverMissing: $serverMissing, ') + ..write('localDirty: $localDirty, ') + ..write('pendingDelete: $pendingDelete, ') + ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') + ..write('createdLocalAtUtc: $createdLocalAtUtc, ') + ..write('updatedLocalAtUtc: $updatedLocalAtUtc') ..write(')')) .toString(); } @override - int get hashCode => Object.hashAll([ - id, + int get hashCode => Object.hash( accountId, - calendarSourceId, - provider, - providerCalendarId, - providerEventId, - providerRecurringEventId, - providerOriginalStartKey, - etagOrChangeKey, - status, + id, + davCollectionId, + kind, + etag, title, - description, - location, - allDay, - startDate, - startDateTime, - startTimeZone, - endDate, - endDateTime, - endTimeZone, - recurrenceJson, - remindersJson, - attendeesJson, - categoriesJson, - organizerJson, - creatorJson, - colorId, - colorHex, - visibility, - transparencyOrShowAs, - eventType, - webLink, - conferenceJson, - attachmentsJson, - isCancelled, - isDeleted, + updatedUtc, + selfLink, rawJson, - createdAtServer, - updatedAtServer, - createdAtLocal, - updatedAtLocal, - syncStatus, - baselineRawJson, - ]); + providerListKind, + isOwner, + isShared, + deltaLink, + providerMetadataJson, + serverMissing, + localDirty, + pendingDelete, + lastSyncedAtUtc, + createdLocalAtUtc, + updatedLocalAtUtc, + ); @override bool operator ==(Object other) => identical(this, other) || - (other is CalendarEvent && - other.id == this.id && + (other is TaskList && other.accountId == this.accountId && - other.calendarSourceId == this.calendarSourceId && - other.provider == this.provider && - other.providerCalendarId == this.providerCalendarId && - other.providerEventId == this.providerEventId && - other.providerRecurringEventId == this.providerRecurringEventId && - other.providerOriginalStartKey == this.providerOriginalStartKey && - other.etagOrChangeKey == this.etagOrChangeKey && - other.status == this.status && + other.id == this.id && + other.davCollectionId == this.davCollectionId && + other.kind == this.kind && + other.etag == this.etag && other.title == this.title && - other.description == this.description && - other.location == this.location && - other.allDay == this.allDay && - other.startDate == this.startDate && - other.startDateTime == this.startDateTime && - other.startTimeZone == this.startTimeZone && - other.endDate == this.endDate && - other.endDateTime == this.endDateTime && - other.endTimeZone == this.endTimeZone && - other.recurrenceJson == this.recurrenceJson && - other.remindersJson == this.remindersJson && - other.attendeesJson == this.attendeesJson && - other.categoriesJson == this.categoriesJson && - other.organizerJson == this.organizerJson && - other.creatorJson == this.creatorJson && - other.colorId == this.colorId && - other.colorHex == this.colorHex && - other.visibility == this.visibility && - other.transparencyOrShowAs == this.transparencyOrShowAs && - other.eventType == this.eventType && - other.webLink == this.webLink && - other.conferenceJson == this.conferenceJson && - other.attachmentsJson == this.attachmentsJson && - other.isCancelled == this.isCancelled && - other.isDeleted == this.isDeleted && + other.updatedUtc == this.updatedUtc && + other.selfLink == this.selfLink && other.rawJson == this.rawJson && - other.createdAtServer == this.createdAtServer && - other.updatedAtServer == this.updatedAtServer && - other.createdAtLocal == this.createdAtLocal && - other.updatedAtLocal == this.updatedAtLocal && - other.syncStatus == this.syncStatus && - other.baselineRawJson == this.baselineRawJson); + other.providerListKind == this.providerListKind && + other.isOwner == this.isOwner && + other.isShared == this.isShared && + other.deltaLink == this.deltaLink && + other.providerMetadataJson == this.providerMetadataJson && + other.serverMissing == this.serverMissing && + other.localDirty == this.localDirty && + other.pendingDelete == this.pendingDelete && + other.lastSyncedAtUtc == this.lastSyncedAtUtc && + other.createdLocalAtUtc == this.createdLocalAtUtc && + other.updatedLocalAtUtc == this.updatedLocalAtUtc); } -class CalendarEventsCompanion extends UpdateCompanion { - final Value id; +class TaskListsCompanion extends UpdateCompanion { final Value accountId; - final Value calendarSourceId; - final Value provider; - final Value providerCalendarId; - final Value providerEventId; - final Value providerRecurringEventId; - final Value providerOriginalStartKey; - final Value etagOrChangeKey; - final Value status; + final Value id; + final Value davCollectionId; + final Value kind; + final Value etag; final Value title; - final Value description; - final Value location; - final Value allDay; - final Value startDate; - final Value startDateTime; - final Value startTimeZone; - final Value endDate; - final Value endDateTime; - final Value endTimeZone; - final Value recurrenceJson; - final Value remindersJson; - final Value attendeesJson; - final Value categoriesJson; - final Value organizerJson; - final Value creatorJson; - final Value colorId; - final Value colorHex; - final Value visibility; - final Value transparencyOrShowAs; - final Value eventType; - final Value webLink; - final Value conferenceJson; - final Value attachmentsJson; - final Value isCancelled; - final Value isDeleted; - final Value rawJson; - final Value createdAtServer; - final Value updatedAtServer; - final Value createdAtLocal; - final Value updatedAtLocal; - final Value syncStatus; - final Value baselineRawJson; + final Value updatedUtc; + final Value selfLink; + final Value rawJson; + final Value providerListKind; + final Value isOwner; + final Value isShared; + final Value deltaLink; + final Value providerMetadataJson; + final Value serverMissing; + final Value localDirty; + final Value pendingDelete; + final Value lastSyncedAtUtc; + final Value createdLocalAtUtc; + final Value updatedLocalAtUtc; final Value rowid; - const CalendarEventsCompanion({ - this.id = const Value.absent(), + const TaskListsCompanion({ this.accountId = const Value.absent(), - this.calendarSourceId = const Value.absent(), - this.provider = const Value.absent(), - this.providerCalendarId = const Value.absent(), - this.providerEventId = const Value.absent(), - this.providerRecurringEventId = const Value.absent(), - this.providerOriginalStartKey = const Value.absent(), - this.etagOrChangeKey = const Value.absent(), - this.status = const Value.absent(), + this.id = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.kind = const Value.absent(), + this.etag = const Value.absent(), this.title = const Value.absent(), - this.description = const Value.absent(), - this.location = const Value.absent(), - this.allDay = const Value.absent(), - this.startDate = const Value.absent(), - this.startDateTime = const Value.absent(), - this.startTimeZone = const Value.absent(), - this.endDate = const Value.absent(), - this.endDateTime = const Value.absent(), - this.endTimeZone = const Value.absent(), - this.recurrenceJson = const Value.absent(), - this.remindersJson = const Value.absent(), - this.attendeesJson = const Value.absent(), - this.categoriesJson = const Value.absent(), - this.organizerJson = const Value.absent(), - this.creatorJson = const Value.absent(), - this.colorId = const Value.absent(), - this.colorHex = const Value.absent(), - this.visibility = const Value.absent(), - this.transparencyOrShowAs = const Value.absent(), - this.eventType = const Value.absent(), - this.webLink = const Value.absent(), - this.conferenceJson = const Value.absent(), - this.attachmentsJson = const Value.absent(), - this.isCancelled = const Value.absent(), - this.isDeleted = const Value.absent(), + this.updatedUtc = const Value.absent(), + this.selfLink = const Value.absent(), this.rawJson = const Value.absent(), - this.createdAtServer = const Value.absent(), - this.updatedAtServer = const Value.absent(), - this.createdAtLocal = const Value.absent(), - this.updatedAtLocal = const Value.absent(), - this.syncStatus = const Value.absent(), - this.baselineRawJson = const Value.absent(), + this.providerListKind = const Value.absent(), + this.isOwner = const Value.absent(), + this.isShared = const Value.absent(), + this.deltaLink = const Value.absent(), + this.providerMetadataJson = const Value.absent(), + this.serverMissing = const Value.absent(), + this.localDirty = const Value.absent(), + this.pendingDelete = const Value.absent(), + this.lastSyncedAtUtc = const Value.absent(), + this.createdLocalAtUtc = const Value.absent(), + this.updatedLocalAtUtc = const Value.absent(), this.rowid = const Value.absent(), }); - CalendarEventsCompanion.insert({ - required String id, + TaskListsCompanion.insert({ required String accountId, - required String calendarSourceId, - required String provider, - required String providerCalendarId, - required String providerEventId, - this.providerRecurringEventId = const Value.absent(), - this.providerOriginalStartKey = const Value.absent(), - this.etagOrChangeKey = const Value.absent(), - this.status = const Value.absent(), + required String id, + this.davCollectionId = const Value.absent(), + this.kind = const Value.absent(), + this.etag = const Value.absent(), required String title, - this.description = const Value.absent(), - this.location = const Value.absent(), - this.allDay = const Value.absent(), - this.startDate = const Value.absent(), - this.startDateTime = const Value.absent(), - this.startTimeZone = const Value.absent(), - this.endDate = const Value.absent(), - this.endDateTime = const Value.absent(), - this.endTimeZone = const Value.absent(), - this.recurrenceJson = const Value.absent(), - this.remindersJson = const Value.absent(), - this.attendeesJson = const Value.absent(), - this.categoriesJson = const Value.absent(), - this.organizerJson = const Value.absent(), - this.creatorJson = const Value.absent(), - this.colorId = const Value.absent(), - this.colorHex = const Value.absent(), - this.visibility = const Value.absent(), - this.transparencyOrShowAs = const Value.absent(), - this.eventType = const Value.absent(), - this.webLink = const Value.absent(), - this.conferenceJson = const Value.absent(), - this.attachmentsJson = const Value.absent(), - this.isCancelled = const Value.absent(), - this.isDeleted = const Value.absent(), - this.rawJson = const Value.absent(), - this.createdAtServer = const Value.absent(), - this.updatedAtServer = const Value.absent(), - required int createdAtLocal, - required int updatedAtLocal, - this.syncStatus = const Value.absent(), - this.baselineRawJson = const Value.absent(), + this.updatedUtc = const Value.absent(), + this.selfLink = const Value.absent(), + required String rawJson, + this.providerListKind = const Value.absent(), + this.isOwner = const Value.absent(), + this.isShared = const Value.absent(), + this.deltaLink = const Value.absent(), + this.providerMetadataJson = const Value.absent(), + this.serverMissing = const Value.absent(), + this.localDirty = const Value.absent(), + this.pendingDelete = const Value.absent(), + this.lastSyncedAtUtc = const Value.absent(), + required String createdLocalAtUtc, + required String updatedLocalAtUtc, this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - calendarSourceId = Value(calendarSourceId), - provider = Value(provider), - providerCalendarId = Value(providerCalendarId), - providerEventId = Value(providerEventId), + }) : accountId = Value(accountId), + id = Value(id), title = Value(title), - createdAtLocal = Value(createdAtLocal), - updatedAtLocal = Value(updatedAtLocal); - static Insertable custom({ - Expression? id, + rawJson = Value(rawJson), + createdLocalAtUtc = Value(createdLocalAtUtc), + updatedLocalAtUtc = Value(updatedLocalAtUtc); + static Insertable custom({ Expression? accountId, - Expression? calendarSourceId, - Expression? provider, - Expression? providerCalendarId, - Expression? providerEventId, - Expression? providerRecurringEventId, - Expression? providerOriginalStartKey, - Expression? etagOrChangeKey, - Expression? status, + Expression? id, + Expression? davCollectionId, + Expression? kind, + Expression? etag, Expression? title, - Expression? description, - Expression? location, - Expression? allDay, - Expression? startDate, - Expression? startDateTime, - Expression? startTimeZone, - Expression? endDate, - Expression? endDateTime, - Expression? endTimeZone, - Expression? recurrenceJson, - Expression? remindersJson, - Expression? attendeesJson, - Expression? categoriesJson, - Expression? organizerJson, - Expression? creatorJson, - Expression? colorId, - Expression? colorHex, - Expression? visibility, - Expression? transparencyOrShowAs, - Expression? eventType, - Expression? webLink, - Expression? conferenceJson, - Expression? attachmentsJson, - Expression? isCancelled, - Expression? isDeleted, + Expression? updatedUtc, + Expression? selfLink, Expression? rawJson, - Expression? createdAtServer, - Expression? updatedAtServer, - Expression? createdAtLocal, - Expression? updatedAtLocal, - Expression? syncStatus, - Expression? baselineRawJson, + Expression? providerListKind, + Expression? isOwner, + Expression? isShared, + Expression? deltaLink, + Expression? providerMetadataJson, + Expression? serverMissing, + Expression? localDirty, + Expression? pendingDelete, + Expression? lastSyncedAtUtc, + Expression? createdLocalAtUtc, + Expression? updatedLocalAtUtc, Expression? rowid, }) { return RawValuesInsertable({ - if (id != null) 'id': id, if (accountId != null) 'account_id': accountId, - if (calendarSourceId != null) 'calendar_source_id': calendarSourceId, - if (provider != null) 'provider': provider, - if (providerCalendarId != null) - 'provider_calendar_id': providerCalendarId, - if (providerEventId != null) 'provider_event_id': providerEventId, - if (providerRecurringEventId != null) - 'provider_recurring_event_id': providerRecurringEventId, - if (providerOriginalStartKey != null) - 'provider_original_start_key': providerOriginalStartKey, - if (etagOrChangeKey != null) 'etag_or_change_key': etagOrChangeKey, - if (status != null) 'status': status, + if (id != null) 'id': id, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (kind != null) 'kind': kind, + if (etag != null) 'etag': etag, if (title != null) 'title': title, - if (description != null) 'description': description, - if (location != null) 'location': location, - if (allDay != null) 'all_day': allDay, - if (startDate != null) 'start_date': startDate, - if (startDateTime != null) 'start_date_time': startDateTime, - if (startTimeZone != null) 'start_time_zone': startTimeZone, - if (endDate != null) 'end_date': endDate, - if (endDateTime != null) 'end_date_time': endDateTime, - if (endTimeZone != null) 'end_time_zone': endTimeZone, - if (recurrenceJson != null) 'recurrence_json': recurrenceJson, - if (remindersJson != null) 'reminders_json': remindersJson, - if (attendeesJson != null) 'attendees_json': attendeesJson, - if (categoriesJson != null) 'categories_json': categoriesJson, - if (organizerJson != null) 'organizer_json': organizerJson, - if (creatorJson != null) 'creator_json': creatorJson, - if (colorId != null) 'color_id': colorId, - if (colorHex != null) 'color_hex': colorHex, - if (visibility != null) 'visibility': visibility, - if (transparencyOrShowAs != null) - 'transparency_or_show_as': transparencyOrShowAs, - if (eventType != null) 'event_type': eventType, - if (webLink != null) 'web_link': webLink, - if (conferenceJson != null) 'conference_json': conferenceJson, - if (attachmentsJson != null) 'attachments_json': attachmentsJson, - if (isCancelled != null) 'is_cancelled': isCancelled, - if (isDeleted != null) 'is_deleted': isDeleted, + if (updatedUtc != null) 'updated_utc': updatedUtc, + if (selfLink != null) 'self_link': selfLink, if (rawJson != null) 'raw_json': rawJson, - if (createdAtServer != null) 'created_at_server': createdAtServer, - if (updatedAtServer != null) 'updated_at_server': updatedAtServer, - if (createdAtLocal != null) 'created_at_local': createdAtLocal, - if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, - if (syncStatus != null) 'sync_status': syncStatus, - if (baselineRawJson != null) 'baseline_raw_json': baselineRawJson, + if (providerListKind != null) 'provider_list_kind': providerListKind, + if (isOwner != null) 'is_owner': isOwner, + if (isShared != null) 'is_shared': isShared, + if (deltaLink != null) 'delta_link': deltaLink, + if (providerMetadataJson != null) + 'provider_metadata_json': providerMetadataJson, + if (serverMissing != null) 'server_missing': serverMissing, + if (localDirty != null) 'local_dirty': localDirty, + if (pendingDelete != null) 'pending_delete': pendingDelete, + if (lastSyncedAtUtc != null) 'last_synced_at_utc': lastSyncedAtUtc, + if (createdLocalAtUtc != null) 'created_local_at_utc': createdLocalAtUtc, + if (updatedLocalAtUtc != null) 'updated_local_at_utc': updatedLocalAtUtc, if (rowid != null) 'rowid': rowid, }); } - CalendarEventsCompanion copyWith({ - Value? id, + TaskListsCompanion copyWith({ Value? accountId, - Value? calendarSourceId, - Value? provider, - Value? providerCalendarId, - Value? providerEventId, - Value? providerRecurringEventId, - Value? providerOriginalStartKey, - Value? etagOrChangeKey, - Value? status, + Value? id, + Value? davCollectionId, + Value? kind, + Value? etag, Value? title, - Value? description, - Value? location, - Value? allDay, - Value? startDate, - Value? startDateTime, - Value? startTimeZone, - Value? endDate, - Value? endDateTime, - Value? endTimeZone, - Value? recurrenceJson, - Value? remindersJson, - Value? attendeesJson, - Value? categoriesJson, - Value? organizerJson, - Value? creatorJson, - Value? colorId, - Value? colorHex, - Value? visibility, - Value? transparencyOrShowAs, - Value? eventType, - Value? webLink, - Value? conferenceJson, - Value? attachmentsJson, - Value? isCancelled, - Value? isDeleted, - Value? rawJson, - Value? createdAtServer, - Value? updatedAtServer, - Value? createdAtLocal, - Value? updatedAtLocal, - Value? syncStatus, - Value? baselineRawJson, + Value? updatedUtc, + Value? selfLink, + Value? rawJson, + Value? providerListKind, + Value? isOwner, + Value? isShared, + Value? deltaLink, + Value? providerMetadataJson, + Value? serverMissing, + Value? localDirty, + Value? pendingDelete, + Value? lastSyncedAtUtc, + Value? createdLocalAtUtc, + Value? updatedLocalAtUtc, Value? rowid, }) { - return CalendarEventsCompanion( - id: id ?? this.id, + return TaskListsCompanion( accountId: accountId ?? this.accountId, - calendarSourceId: calendarSourceId ?? this.calendarSourceId, - provider: provider ?? this.provider, - providerCalendarId: providerCalendarId ?? this.providerCalendarId, - providerEventId: providerEventId ?? this.providerEventId, - providerRecurringEventId: - providerRecurringEventId ?? this.providerRecurringEventId, - providerOriginalStartKey: - providerOriginalStartKey ?? this.providerOriginalStartKey, - etagOrChangeKey: etagOrChangeKey ?? this.etagOrChangeKey, - status: status ?? this.status, + id: id ?? this.id, + davCollectionId: davCollectionId ?? this.davCollectionId, + kind: kind ?? this.kind, + etag: etag ?? this.etag, title: title ?? this.title, - description: description ?? this.description, - location: location ?? this.location, - allDay: allDay ?? this.allDay, - startDate: startDate ?? this.startDate, - startDateTime: startDateTime ?? this.startDateTime, - startTimeZone: startTimeZone ?? this.startTimeZone, - endDate: endDate ?? this.endDate, - endDateTime: endDateTime ?? this.endDateTime, - endTimeZone: endTimeZone ?? this.endTimeZone, - recurrenceJson: recurrenceJson ?? this.recurrenceJson, - remindersJson: remindersJson ?? this.remindersJson, - attendeesJson: attendeesJson ?? this.attendeesJson, - categoriesJson: categoriesJson ?? this.categoriesJson, - organizerJson: organizerJson ?? this.organizerJson, - creatorJson: creatorJson ?? this.creatorJson, - colorId: colorId ?? this.colorId, - colorHex: colorHex ?? this.colorHex, - visibility: visibility ?? this.visibility, - transparencyOrShowAs: transparencyOrShowAs ?? this.transparencyOrShowAs, - eventType: eventType ?? this.eventType, - webLink: webLink ?? this.webLink, - conferenceJson: conferenceJson ?? this.conferenceJson, - attachmentsJson: attachmentsJson ?? this.attachmentsJson, - isCancelled: isCancelled ?? this.isCancelled, - isDeleted: isDeleted ?? this.isDeleted, + updatedUtc: updatedUtc ?? this.updatedUtc, + selfLink: selfLink ?? this.selfLink, rawJson: rawJson ?? this.rawJson, - createdAtServer: createdAtServer ?? this.createdAtServer, - updatedAtServer: updatedAtServer ?? this.updatedAtServer, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - syncStatus: syncStatus ?? this.syncStatus, - baselineRawJson: baselineRawJson ?? this.baselineRawJson, + providerListKind: providerListKind ?? this.providerListKind, + isOwner: isOwner ?? this.isOwner, + isShared: isShared ?? this.isShared, + deltaLink: deltaLink ?? this.deltaLink, + providerMetadataJson: providerMetadataJson ?? this.providerMetadataJson, + serverMissing: serverMissing ?? this.serverMissing, + localDirty: localDirty ?? this.localDirty, + pendingDelete: pendingDelete ?? this.pendingDelete, + lastSyncedAtUtc: lastSyncedAtUtc ?? this.lastSyncedAtUtc, + createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, + updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, rowid: rowid ?? this.rowid, ); } @@ -10173,140 +7870,67 @@ class CalendarEventsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } if (accountId.present) { map['account_id'] = Variable(accountId.value); } - if (calendarSourceId.present) { - map['calendar_source_id'] = Variable(calendarSourceId.value); - } - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (providerCalendarId.present) { - map['provider_calendar_id'] = Variable(providerCalendarId.value); - } - if (providerEventId.present) { - map['provider_event_id'] = Variable(providerEventId.value); - } - if (providerRecurringEventId.present) { - map['provider_recurring_event_id'] = Variable( - providerRecurringEventId.value, - ); + if (id.present) { + map['id'] = Variable(id.value); } - if (providerOriginalStartKey.present) { - map['provider_original_start_key'] = Variable( - providerOriginalStartKey.value, - ); + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); } - if (etagOrChangeKey.present) { - map['etag_or_change_key'] = Variable(etagOrChangeKey.value); + if (kind.present) { + map['kind'] = Variable(kind.value); } - if (status.present) { - map['status'] = Variable(status.value); + if (etag.present) { + map['etag'] = Variable(etag.value); } if (title.present) { map['title'] = Variable(title.value); } - if (description.present) { - map['description'] = Variable(description.value); + if (updatedUtc.present) { + map['updated_utc'] = Variable(updatedUtc.value); } - if (location.present) { - map['location'] = Variable(location.value); + if (selfLink.present) { + map['self_link'] = Variable(selfLink.value); } - if (allDay.present) { - map['all_day'] = Variable(allDay.value); + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); } - if (startDate.present) { - map['start_date'] = Variable(startDate.value); + if (providerListKind.present) { + map['provider_list_kind'] = Variable(providerListKind.value); } - if (startDateTime.present) { - map['start_date_time'] = Variable(startDateTime.value); - } - if (startTimeZone.present) { - map['start_time_zone'] = Variable(startTimeZone.value); - } - if (endDate.present) { - map['end_date'] = Variable(endDate.value); - } - if (endDateTime.present) { - map['end_date_time'] = Variable(endDateTime.value); - } - if (endTimeZone.present) { - map['end_time_zone'] = Variable(endTimeZone.value); - } - if (recurrenceJson.present) { - map['recurrence_json'] = Variable(recurrenceJson.value); - } - if (remindersJson.present) { - map['reminders_json'] = Variable(remindersJson.value); - } - if (attendeesJson.present) { - map['attendees_json'] = Variable(attendeesJson.value); - } - if (categoriesJson.present) { - map['categories_json'] = Variable(categoriesJson.value); - } - if (organizerJson.present) { - map['organizer_json'] = Variable(organizerJson.value); - } - if (creatorJson.present) { - map['creator_json'] = Variable(creatorJson.value); - } - if (colorId.present) { - map['color_id'] = Variable(colorId.value); + if (isOwner.present) { + map['is_owner'] = Variable(isOwner.value); } - if (colorHex.present) { - map['color_hex'] = Variable(colorHex.value); + if (isShared.present) { + map['is_shared'] = Variable(isShared.value); } - if (visibility.present) { - map['visibility'] = Variable(visibility.value); + if (deltaLink.present) { + map['delta_link'] = Variable(deltaLink.value); } - if (transparencyOrShowAs.present) { - map['transparency_or_show_as'] = Variable( - transparencyOrShowAs.value, + if (providerMetadataJson.present) { + map['provider_metadata_json'] = Variable( + providerMetadataJson.value, ); } - if (eventType.present) { - map['event_type'] = Variable(eventType.value); - } - if (webLink.present) { - map['web_link'] = Variable(webLink.value); - } - if (conferenceJson.present) { - map['conference_json'] = Variable(conferenceJson.value); - } - if (attachmentsJson.present) { - map['attachments_json'] = Variable(attachmentsJson.value); - } - if (isCancelled.present) { - map['is_cancelled'] = Variable(isCancelled.value); - } - if (isDeleted.present) { - map['is_deleted'] = Variable(isDeleted.value); - } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); - } - if (createdAtServer.present) { - map['created_at_server'] = Variable(createdAtServer.value); + if (serverMissing.present) { + map['server_missing'] = Variable(serverMissing.value); } - if (updatedAtServer.present) { - map['updated_at_server'] = Variable(updatedAtServer.value); + if (localDirty.present) { + map['local_dirty'] = Variable(localDirty.value); } - if (createdAtLocal.present) { - map['created_at_local'] = Variable(createdAtLocal.value); + if (pendingDelete.present) { + map['pending_delete'] = Variable(pendingDelete.value); } - if (updatedAtLocal.present) { - map['updated_at_local'] = Variable(updatedAtLocal.value); + if (lastSyncedAtUtc.present) { + map['last_synced_at_utc'] = Variable(lastSyncedAtUtc.value); } - if (syncStatus.present) { - map['sync_status'] = Variable(syncStatus.value); + if (createdLocalAtUtc.present) { + map['created_local_at_utc'] = Variable(createdLocalAtUtc.value); } - if (baselineRawJson.present) { - map['baseline_raw_json'] = Variable(baselineRawJson.value); + if (updatedLocalAtUtc.present) { + map['updated_local_at_utc'] = Variable(updatedLocalAtUtc.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -10316,1366 +7940,871 @@ class CalendarEventsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('CalendarEventsCompanion(') - ..write('id: $id, ') + return (StringBuffer('TaskListsCompanion(') ..write('accountId: $accountId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('provider: $provider, ') - ..write('providerCalendarId: $providerCalendarId, ') - ..write('providerEventId: $providerEventId, ') - ..write('providerRecurringEventId: $providerRecurringEventId, ') - ..write('providerOriginalStartKey: $providerOriginalStartKey, ') - ..write('etagOrChangeKey: $etagOrChangeKey, ') - ..write('status: $status, ') + ..write('id: $id, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('kind: $kind, ') + ..write('etag: $etag, ') ..write('title: $title, ') - ..write('description: $description, ') - ..write('location: $location, ') - ..write('allDay: $allDay, ') - ..write('startDate: $startDate, ') - ..write('startDateTime: $startDateTime, ') - ..write('startTimeZone: $startTimeZone, ') - ..write('endDate: $endDate, ') - ..write('endDateTime: $endDateTime, ') - ..write('endTimeZone: $endTimeZone, ') - ..write('recurrenceJson: $recurrenceJson, ') - ..write('remindersJson: $remindersJson, ') - ..write('attendeesJson: $attendeesJson, ') - ..write('categoriesJson: $categoriesJson, ') - ..write('organizerJson: $organizerJson, ') - ..write('creatorJson: $creatorJson, ') - ..write('colorId: $colorId, ') - ..write('colorHex: $colorHex, ') - ..write('visibility: $visibility, ') - ..write('transparencyOrShowAs: $transparencyOrShowAs, ') - ..write('eventType: $eventType, ') - ..write('webLink: $webLink, ') - ..write('conferenceJson: $conferenceJson, ') - ..write('attachmentsJson: $attachmentsJson, ') - ..write('isCancelled: $isCancelled, ') - ..write('isDeleted: $isDeleted, ') + ..write('updatedUtc: $updatedUtc, ') + ..write('selfLink: $selfLink, ') ..write('rawJson: $rawJson, ') - ..write('createdAtServer: $createdAtServer, ') - ..write('updatedAtServer: $updatedAtServer, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal, ') - ..write('syncStatus: $syncStatus, ') - ..write('baselineRawJson: $baselineRawJson, ') + ..write('providerListKind: $providerListKind, ') + ..write('isOwner: $isOwner, ') + ..write('isShared: $isShared, ') + ..write('deltaLink: $deltaLink, ') + ..write('providerMetadataJson: $providerMetadataJson, ') + ..write('serverMissing: $serverMissing, ') + ..write('localDirty: $localDirty, ') + ..write('pendingDelete: $pendingDelete, ') + ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') + ..write('createdLocalAtUtc: $createdLocalAtUtc, ') + ..write('updatedLocalAtUtc: $updatedLocalAtUtc, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $CalendarEventAttendeesTable extends CalendarEventAttendees - with TableInfo<$CalendarEventAttendeesTable, CalendarEventAttendee> { +class $TasksTable extends Tasks with TableInfo<$TasksTable, Task> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $CalendarEventAttendeesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); + $TasksTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), ); - static const VerificationMeta _calendarEventIdMeta = const VerificationMeta( - 'calendarEventId', + static const VerificationMeta _taskListIdMeta = const VerificationMeta( + 'taskListId', ); @override - late final GeneratedColumn calendarEventId = GeneratedColumn( - 'calendar_event_id', + late final GeneratedColumn taskListId = GeneratedColumn( + 'task_list_id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES calendar_events (id) ON DELETE CASCADE', - ), ); - static const VerificationMeta _emailMeta = const VerificationMeta('email'); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn email = GeneratedColumn( - 'email', + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _displayNameMeta = const VerificationMeta( - 'displayName', + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', ); @override - late final GeneratedColumn displayName = GeneratedColumn( - 'display_name', + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), ); - static const VerificationMeta _responseStatusMeta = const VerificationMeta( - 'responseStatus', + static const VerificationMeta _davObjectIdMeta = const VerificationMeta( + 'davObjectId', ); @override - late final GeneratedColumn responseStatus = GeneratedColumn( - 'response_status', + late final GeneratedColumn davObjectId = GeneratedColumn( + 'dav_object_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_objects (id) ON DELETE SET NULL', + ), ); - static const VerificationMeta _optionalMeta = const VerificationMeta( - 'optional', + static const VerificationMeta _davComponentIdMeta = const VerificationMeta( + 'davComponentId', ); @override - late final GeneratedColumn optional = GeneratedColumn( - 'optional', + late final GeneratedColumn davComponentId = GeneratedColumn( + 'dav_component_id', aliasedName, - false, - type: DriftSqlType.bool, + true, + type: DriftSqlType.string, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("optional" IN (0, 1))', + 'REFERENCES dav_object_components (id) ON DELETE SET NULL', ), - defaultValue: const Constant(false), ); - static const VerificationMeta _organizerMeta = const VerificationMeta( - 'organizer', + static const VerificationMeta _icalUidMeta = const VerificationMeta( + 'icalUid', ); @override - late final GeneratedColumn organizer = GeneratedColumn( - 'organizer', + late final GeneratedColumn icalUid = GeneratedColumn( + 'ical_uid', aliasedName, - false, - type: DriftSqlType.bool, + true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("organizer" IN (0, 1))', - ), - defaultValue: const Constant(false), ); - static const VerificationMeta _selfMeta = const VerificationMeta('self'); + static const VerificationMeta _recurrenceIdKeyMeta = const VerificationMeta( + 'recurrenceIdKey', + ); @override - late final GeneratedColumn self = GeneratedColumn( - 'self', + late final GeneratedColumn recurrenceIdKey = GeneratedColumn( + 'recurrence_id_key', aliasedName, - false, - type: DriftSqlType.bool, + true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("self" IN (0, 1))', - ), - defaultValue: const Constant(false), ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', + static const VerificationMeta _icalPriorityMeta = const VerificationMeta( + 'icalPriority', ); @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', + late final GeneratedColumn icalPriority = GeneratedColumn( + 'ical_priority', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _percentCompleteMeta = const VerificationMeta( + 'percentComplete', + ); + @override + late final GeneratedColumn percentComplete = GeneratedColumn( + 'percent_complete', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _taskLocationMeta = const VerificationMeta( + 'taskLocation', + ); + @override + late final GeneratedColumn taskLocation = GeneratedColumn( + 'task_location', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _taskUrlMeta = const VerificationMeta( + 'taskUrl', + ); @override - List get $columns => [ - id, - calendarEventId, - email, - displayName, - responseStatus, - optional, - organizer, - self, - rawJson, - ]; + late final GeneratedColumn taskUrl = GeneratedColumn( + 'task_url', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _taskClassificationMeta = + const VerificationMeta('taskClassification'); @override - String get aliasedName => _alias ?? actualTableName; + late final GeneratedColumn taskClassification = + GeneratedColumn( + 'task_classification', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _taskPinnedMeta = const VerificationMeta( + 'taskPinned', + ); @override - String get actualTableName => $name; - static const String $name = 'calendar_event_attendees'; + late final GeneratedColumn taskPinned = GeneratedColumn( + 'task_pinned', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("task_pinned" IN (0, 1))', + ), + ); + static const VerificationMeta _taskHideSubtasksMeta = const VerificationMeta( + 'taskHideSubtasks', + ); @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } - if (data.containsKey('calendar_event_id')) { - context.handle( - _calendarEventIdMeta, - calendarEventId.isAcceptableOrUnknown( - data['calendar_event_id']!, - _calendarEventIdMeta, - ), - ); - } else if (isInserting) { - context.missing(_calendarEventIdMeta); - } - if (data.containsKey('email')) { - context.handle( - _emailMeta, - email.isAcceptableOrUnknown(data['email']!, _emailMeta), - ); - } else if (isInserting) { - context.missing(_emailMeta); - } - if (data.containsKey('display_name')) { - context.handle( - _displayNameMeta, - displayName.isAcceptableOrUnknown( - data['display_name']!, - _displayNameMeta, - ), - ); - } - if (data.containsKey('response_status')) { - context.handle( - _responseStatusMeta, - responseStatus.isAcceptableOrUnknown( - data['response_status']!, - _responseStatusMeta, + late final GeneratedColumn taskHideSubtasks = GeneratedColumn( + 'task_hide_subtasks', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("task_hide_subtasks" IN (0, 1))', + ), + ); + static const VerificationMeta _taskHideCompletedSubtasksMeta = + const VerificationMeta('taskHideCompletedSubtasks'); + @override + late final GeneratedColumn taskHideCompletedSubtasks = + GeneratedColumn( + 'task_hide_completed_subtasks', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("task_hide_completed_subtasks" IN (0, 1))', ), ); - } - if (data.containsKey('optional')) { - context.handle( - _optionalMeta, - optional.isAcceptableOrUnknown(data['optional']!, _optionalMeta), - ); - } - if (data.containsKey('organizer')) { - context.handle( - _organizerMeta, - organizer.isAcceptableOrUnknown(data['organizer']!, _organizerMeta), - ); - } - if (data.containsKey('self')) { - context.handle( - _selfMeta, - self.isAcceptableOrUnknown(data['self']!, _selfMeta), - ); - } - if (data.containsKey('raw_json')) { - context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + static const VerificationMeta _taskAlarmsJsonMeta = const VerificationMeta( + 'taskAlarmsJson', + ); + @override + late final GeneratedColumn taskAlarmsJson = GeneratedColumn( + 'task_alarms_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _parentUidMeta = const VerificationMeta( + 'parentUid', + ); + @override + late final GeneratedColumn parentUid = GeneratedColumn( + 'parent_uid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sortOrderMeta = const VerificationMeta( + 'sortOrder', + ); + @override + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _providerExtensionProjectionJsonMeta = + const VerificationMeta('providerExtensionProjectionJson'); + @override + late final GeneratedColumn providerExtensionProjectionJson = + GeneratedColumn( + 'provider_extension_projection_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } - return context; - } - + static const VerificationMeta _projectionVersionMeta = const VerificationMeta( + 'projectionVersion', + ); @override - Set get $primaryKey => {id}; + late final GeneratedColumn projectionVersion = GeneratedColumn( + 'projection_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); + static const VerificationMeta _kindMeta = const VerificationMeta('kind'); @override - CalendarEventAttendee map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarEventAttendee( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, - calendarEventId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}calendar_event_id'], - )!, - email: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}email'], - )!, - displayName: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}display_name'], - ), - responseStatus: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}response_status'], - ), - optional: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}optional'], - )!, - organizer: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}organizer'], - )!, - self: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}self'], - )!, - rawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_json'], - ), - ); - } - + late final GeneratedColumn kind = GeneratedColumn( + 'kind', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _etagMeta = const VerificationMeta('etag'); @override - $CalendarEventAttendeesTable createAlias(String alias) { - return $CalendarEventAttendeesTable(attachedDatabase, alias); - } -} - -class CalendarEventAttendee extends DataClass - implements Insertable { - final String id; - final String calendarEventId; - final String email; - final String? displayName; - final String? responseStatus; - final bool optional; - final bool organizer; - final bool self; - final String? rawJson; - const CalendarEventAttendee({ - required this.id, - required this.calendarEventId, - required this.email, - this.displayName, - this.responseStatus, - required this.optional, - required this.organizer, - required this.self, - this.rawJson, - }); + late final GeneratedColumn etag = GeneratedColumn( + 'etag', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _titleMeta = const VerificationMeta('title'); @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['calendar_event_id'] = Variable(calendarEventId); - map['email'] = Variable(email); - if (!nullToAbsent || displayName != null) { - map['display_name'] = Variable(displayName); - } - if (!nullToAbsent || responseStatus != null) { - map['response_status'] = Variable(responseStatus); - } - map['optional'] = Variable(optional); - map['organizer'] = Variable(organizer); - map['self'] = Variable(self); - if (!nullToAbsent || rawJson != null) { - map['raw_json'] = Variable(rawJson); - } - return map; - } - - CalendarEventAttendeesCompanion toCompanion(bool nullToAbsent) { - return CalendarEventAttendeesCompanion( - id: Value(id), - calendarEventId: Value(calendarEventId), - email: Value(email), - displayName: displayName == null && nullToAbsent - ? const Value.absent() - : Value(displayName), - responseStatus: responseStatus == null && nullToAbsent - ? const Value.absent() - : Value(responseStatus), - optional: Value(optional), - organizer: Value(organizer), - self: Value(self), - rawJson: rawJson == null && nullToAbsent - ? const Value.absent() - : Value(rawJson), - ); - } - - factory CalendarEventAttendee.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarEventAttendee( - id: serializer.fromJson(json['id']), - calendarEventId: serializer.fromJson(json['calendarEventId']), - email: serializer.fromJson(json['email']), - displayName: serializer.fromJson(json['displayName']), - responseStatus: serializer.fromJson(json['responseStatus']), - optional: serializer.fromJson(json['optional']), - organizer: serializer.fromJson(json['organizer']), - self: serializer.fromJson(json['self']), - rawJson: serializer.fromJson(json['rawJson']), - ); - } + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedUtcMeta = const VerificationMeta( + 'updatedUtc', + ); @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'calendarEventId': serializer.toJson(calendarEventId), - 'email': serializer.toJson(email), - 'displayName': serializer.toJson(displayName), - 'responseStatus': serializer.toJson(responseStatus), - 'optional': serializer.toJson(optional), - 'organizer': serializer.toJson(organizer), - 'self': serializer.toJson(self), - 'rawJson': serializer.toJson(rawJson), - }; - } - - CalendarEventAttendee copyWith({ - String? id, - String? calendarEventId, - String? email, - Value displayName = const Value.absent(), - Value responseStatus = const Value.absent(), - bool? optional, - bool? organizer, - bool? self, - Value rawJson = const Value.absent(), - }) => CalendarEventAttendee( - id: id ?? this.id, - calendarEventId: calendarEventId ?? this.calendarEventId, - email: email ?? this.email, - displayName: displayName.present ? displayName.value : this.displayName, - responseStatus: responseStatus.present - ? responseStatus.value - : this.responseStatus, - optional: optional ?? this.optional, - organizer: organizer ?? this.organizer, - self: self ?? this.self, - rawJson: rawJson.present ? rawJson.value : this.rawJson, + late final GeneratedColumn updatedUtc = GeneratedColumn( + 'updated_utc', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _selfLinkMeta = const VerificationMeta( + 'selfLink', ); - CalendarEventAttendee copyWithCompanion( - CalendarEventAttendeesCompanion data, - ) { - return CalendarEventAttendee( - id: data.id.present ? data.id.value : this.id, - calendarEventId: data.calendarEventId.present - ? data.calendarEventId.value - : this.calendarEventId, - email: data.email.present ? data.email.value : this.email, - displayName: data.displayName.present - ? data.displayName.value - : this.displayName, - responseStatus: data.responseStatus.present - ? data.responseStatus.value - : this.responseStatus, - optional: data.optional.present ? data.optional.value : this.optional, - organizer: data.organizer.present ? data.organizer.value : this.organizer, - self: data.self.present ? data.self.value : this.self, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - ); - } - @override - String toString() { - return (StringBuffer('CalendarEventAttendee(') - ..write('id: $id, ') - ..write('calendarEventId: $calendarEventId, ') - ..write('email: $email, ') - ..write('displayName: $displayName, ') - ..write('responseStatus: $responseStatus, ') - ..write('optional: $optional, ') - ..write('organizer: $organizer, ') - ..write('self: $self, ') - ..write('rawJson: $rawJson') - ..write(')')) - .toString(); - } - + late final GeneratedColumn selfLink = GeneratedColumn( + 'self_link', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _parentMeta = const VerificationMeta('parent'); @override - int get hashCode => Object.hash( - id, - calendarEventId, - email, - displayName, - responseStatus, - optional, - organizer, - self, - rawJson, + late final GeneratedColumn parent = GeneratedColumn( + 'parent', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _positionMeta = const VerificationMeta( + 'position', ); @override - bool operator ==(Object other) => - identical(this, other) || - (other is CalendarEventAttendee && - other.id == this.id && - other.calendarEventId == this.calendarEventId && - other.email == this.email && - other.displayName == this.displayName && - other.responseStatus == this.responseStatus && - other.optional == this.optional && - other.organizer == this.organizer && - other.self == this.self && - other.rawJson == this.rawJson); -} - -class CalendarEventAttendeesCompanion - extends UpdateCompanion { - final Value id; - final Value calendarEventId; - final Value email; - final Value displayName; - final Value responseStatus; - final Value optional; - final Value organizer; - final Value self; - final Value rawJson; - final Value rowid; - const CalendarEventAttendeesCompanion({ - this.id = const Value.absent(), - this.calendarEventId = const Value.absent(), - this.email = const Value.absent(), - this.displayName = const Value.absent(), - this.responseStatus = const Value.absent(), - this.optional = const Value.absent(), - this.organizer = const Value.absent(), - this.self = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }); - CalendarEventAttendeesCompanion.insert({ - required String id, - required String calendarEventId, - required String email, - this.displayName = const Value.absent(), - this.responseStatus = const Value.absent(), - this.optional = const Value.absent(), - this.organizer = const Value.absent(), - this.self = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }) : id = Value(id), - calendarEventId = Value(calendarEventId), - email = Value(email); - static Insertable custom({ - Expression? id, - Expression? calendarEventId, - Expression? email, - Expression? displayName, - Expression? responseStatus, - Expression? optional, - Expression? organizer, - Expression? self, - Expression? rawJson, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (calendarEventId != null) 'calendar_event_id': calendarEventId, - if (email != null) 'email': email, - if (displayName != null) 'display_name': displayName, - if (responseStatus != null) 'response_status': responseStatus, - if (optional != null) 'optional': optional, - if (organizer != null) 'organizer': organizer, - if (self != null) 'self': self, - if (rawJson != null) 'raw_json': rawJson, - if (rowid != null) 'rowid': rowid, - }); - } - - CalendarEventAttendeesCompanion copyWith({ - Value? id, - Value? calendarEventId, - Value? email, - Value? displayName, - Value? responseStatus, - Value? optional, - Value? organizer, - Value? self, - Value? rawJson, - Value? rowid, - }) { - return CalendarEventAttendeesCompanion( - id: id ?? this.id, - calendarEventId: calendarEventId ?? this.calendarEventId, - email: email ?? this.email, - displayName: displayName ?? this.displayName, - responseStatus: responseStatus ?? this.responseStatus, - optional: optional ?? this.optional, - organizer: organizer ?? this.organizer, - self: self ?? this.self, - rawJson: rawJson ?? this.rawJson, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (calendarEventId.present) { - map['calendar_event_id'] = Variable(calendarEventId.value); - } - if (email.present) { - map['email'] = Variable(email.value); - } - if (displayName.present) { - map['display_name'] = Variable(displayName.value); - } - if (responseStatus.present) { - map['response_status'] = Variable(responseStatus.value); - } - if (optional.present) { - map['optional'] = Variable(optional.value); - } - if (organizer.present) { - map['organizer'] = Variable(organizer.value); - } - if (self.present) { - map['self'] = Variable(self.value); - } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CalendarEventAttendeesCompanion(') - ..write('id: $id, ') - ..write('calendarEventId: $calendarEventId, ') - ..write('email: $email, ') - ..write('displayName: $displayName, ') - ..write('responseStatus: $responseStatus, ') - ..write('optional: $optional, ') - ..write('organizer: $organizer, ') - ..write('self: $self, ') - ..write('rawJson: $rawJson, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $CalendarEventRemindersTable extends CalendarEventReminders - with TableInfo<$CalendarEventRemindersTable, CalendarEventReminder> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $CalendarEventRemindersTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', + late final GeneratedColumn position = GeneratedColumn( + 'position', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _calendarEventIdMeta = const VerificationMeta( - 'calendarEventId', + requiredDuringInsert: false, ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); @override - late final GeneratedColumn calendarEventId = GeneratedColumn( - 'calendar_event_id', + late final GeneratedColumn notes = GeneratedColumn( + 'notes', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES calendar_events (id) ON DELETE CASCADE', - ), - ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', + requiredDuringInsert: false, ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', + late final GeneratedColumn status = GeneratedColumn( + 'status', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, + requiredDuringInsert: false, ); - static const VerificationMeta _methodMeta = const VerificationMeta('method'); + static const VerificationMeta _dueUtcMeta = const VerificationMeta('dueUtc'); @override - late final GeneratedColumn method = GeneratedColumn( - 'method', + late final GeneratedColumn dueUtc = GeneratedColumn( + 'due_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _minutesBeforeMeta = const VerificationMeta( - 'minutesBefore', + static const VerificationMeta _completedUtcMeta = const VerificationMeta( + 'completedUtc', ); @override - late final GeneratedColumn minutesBefore = GeneratedColumn( - 'minutes_before', + late final GeneratedColumn completedUtc = GeneratedColumn( + 'completed_utc', aliasedName, true, - type: DriftSqlType.int, + type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _absoluteTimeMeta = const VerificationMeta( - 'absoluteTime', + static const VerificationMeta _providerStatusMeta = const VerificationMeta( + 'providerStatus', ); @override - late final GeneratedColumn absoluteTime = GeneratedColumn( - 'absolute_time', + late final GeneratedColumn providerStatus = GeneratedColumn( + 'provider_status', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _enabledMeta = const VerificationMeta( - 'enabled', + static const VerificationMeta _bodyContentMeta = const VerificationMeta( + 'bodyContent', ); @override - late final GeneratedColumn enabled = GeneratedColumn( - 'enabled', + late final GeneratedColumn bodyContent = GeneratedColumn( + 'body_content', aliasedName, - false, - type: DriftSqlType.bool, + true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("enabled" IN (0, 1))', - ), - defaultValue: const Constant(true), ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', + static const VerificationMeta _bodyContentTypeMeta = const VerificationMeta( + 'bodyContentType', ); @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', + late final GeneratedColumn bodyContentType = GeneratedColumn( + 'body_content_type', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _microsoftDueDateTimeMeta = + const VerificationMeta('microsoftDueDateTime'); @override - List get $columns => [ - id, - calendarEventId, - provider, - method, - minutesBefore, - absoluteTime, - enabled, - rawJson, - ]; + late final GeneratedColumn microsoftDueDateTime = + GeneratedColumn( + 'microsoft_due_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _microsoftDueTimeZoneMeta = + const VerificationMeta('microsoftDueTimeZone'); @override - String get aliasedName => _alias ?? actualTableName; + late final GeneratedColumn microsoftDueTimeZone = + GeneratedColumn( + 'microsoft_due_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _microsoftStartDateTimeMeta = + const VerificationMeta('microsoftStartDateTime'); @override - String get actualTableName => $name; - static const String $name = 'calendar_event_reminders'; + late final GeneratedColumn microsoftStartDateTime = + GeneratedColumn( + 'microsoft_start_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _microsoftStartTimeZoneMeta = + const VerificationMeta('microsoftStartTimeZone'); @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } - if (data.containsKey('calendar_event_id')) { - context.handle( - _calendarEventIdMeta, - calendarEventId.isAcceptableOrUnknown( - data['calendar_event_id']!, - _calendarEventIdMeta, - ), + late final GeneratedColumn microsoftStartTimeZone = + GeneratedColumn( + 'microsoft_start_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } else if (isInserting) { - context.missing(_calendarEventIdMeta); - } - if (data.containsKey('provider')) { - context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + static const VerificationMeta _microsoftReminderDateTimeMeta = + const VerificationMeta('microsoftReminderDateTime'); + @override + late final GeneratedColumn microsoftReminderDateTime = + GeneratedColumn( + 'microsoft_reminder_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } else if (isInserting) { - context.missing(_providerMeta); - } - if (data.containsKey('method')) { - context.handle( - _methodMeta, - method.isAcceptableOrUnknown(data['method']!, _methodMeta), + static const VerificationMeta _microsoftReminderTimeZoneMeta = + const VerificationMeta('microsoftReminderTimeZone'); + @override + late final GeneratedColumn microsoftReminderTimeZone = + GeneratedColumn( + 'microsoft_reminder_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } - if (data.containsKey('minutes_before')) { - context.handle( - _minutesBeforeMeta, - minutesBefore.isAcceptableOrUnknown( - data['minutes_before']!, - _minutesBeforeMeta, + static const VerificationMeta _microsoftIsReminderOnMeta = + const VerificationMeta('microsoftIsReminderOn'); + @override + late final GeneratedColumn microsoftIsReminderOn = + GeneratedColumn( + 'microsoft_is_reminder_on', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("microsoft_is_reminder_on" IN (0, 1))', ), ); - } - if (data.containsKey('absolute_time')) { - context.handle( - _absoluteTimeMeta, - absoluteTime.isAcceptableOrUnknown( - data['absolute_time']!, - _absoluteTimeMeta, - ), + static const VerificationMeta _microsoftCompletedDateTimeMeta = + const VerificationMeta('microsoftCompletedDateTime'); + @override + late final GeneratedColumn microsoftCompletedDateTime = + GeneratedColumn( + 'microsoft_completed_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } - if (data.containsKey('enabled')) { - context.handle( - _enabledMeta, - enabled.isAcceptableOrUnknown(data['enabled']!, _enabledMeta), + static const VerificationMeta _microsoftCompletedTimeZoneMeta = + const VerificationMeta('microsoftCompletedTimeZone'); + @override + late final GeneratedColumn microsoftCompletedTimeZone = + GeneratedColumn( + 'microsoft_completed_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } - if (data.containsKey('raw_json')) { - context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + static const VerificationMeta _microsoftChecklistItemsJsonMeta = + const VerificationMeta('microsoftChecklistItemsJson'); + @override + late final GeneratedColumn microsoftChecklistItemsJson = + GeneratedColumn( + 'microsoft_checklist_items_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - } - return context; - } - + static const VerificationMeta _recurrenceJsonMeta = const VerificationMeta( + 'recurrenceJson', + ); @override - Set get $primaryKey => {id}; + late final GeneratedColumn recurrenceJson = GeneratedColumn( + 'recurrence_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _importanceMeta = const VerificationMeta( + 'importance', + ); @override - CalendarEventReminder map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarEventReminder( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, - calendarEventId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}calendar_event_id'], - )!, - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - )!, - method: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}method'], - ), - minutesBefore: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}minutes_before'], - ), - absoluteTime: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}absolute_time'], - ), - enabled: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}enabled'], - )!, - rawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_json'], - ), - ); - } - + late final GeneratedColumn importance = GeneratedColumn( + 'importance', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _categoriesJsonMeta = const VerificationMeta( + 'categoriesJson', + ); @override - $CalendarEventRemindersTable createAlias(String alias) { - return $CalendarEventRemindersTable(attachedDatabase, alias); - } -} - -class CalendarEventReminder extends DataClass - implements Insertable { - final String id; - final String calendarEventId; - final String provider; - final String? method; - final int? minutesBefore; - final String? absoluteTime; - final bool enabled; - final String? rawJson; - const CalendarEventReminder({ - required this.id, - required this.calendarEventId, - required this.provider, - this.method, - this.minutesBefore, - this.absoluteTime, - required this.enabled, - this.rawJson, - }); + late final GeneratedColumn categoriesJson = GeneratedColumn( + 'categories_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _hasAttachmentsMeta = const VerificationMeta( + 'hasAttachments', + ); @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['calendar_event_id'] = Variable(calendarEventId); - map['provider'] = Variable(provider); - if (!nullToAbsent || method != null) { - map['method'] = Variable(method); - } - if (!nullToAbsent || minutesBefore != null) { - map['minutes_before'] = Variable(minutesBefore); - } - if (!nullToAbsent || absoluteTime != null) { - map['absolute_time'] = Variable(absoluteTime); - } - map['enabled'] = Variable(enabled); - if (!nullToAbsent || rawJson != null) { - map['raw_json'] = Variable(rawJson); - } - return map; - } - - CalendarEventRemindersCompanion toCompanion(bool nullToAbsent) { - return CalendarEventRemindersCompanion( - id: Value(id), - calendarEventId: Value(calendarEventId), - provider: Value(provider), - method: method == null && nullToAbsent - ? const Value.absent() - : Value(method), - minutesBefore: minutesBefore == null && nullToAbsent - ? const Value.absent() - : Value(minutesBefore), - absoluteTime: absoluteTime == null && nullToAbsent - ? const Value.absent() - : Value(absoluteTime), - enabled: Value(enabled), - rawJson: rawJson == null && nullToAbsent - ? const Value.absent() - : Value(rawJson), - ); - } - - factory CalendarEventReminder.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarEventReminder( - id: serializer.fromJson(json['id']), - calendarEventId: serializer.fromJson(json['calendarEventId']), - provider: serializer.fromJson(json['provider']), - method: serializer.fromJson(json['method']), - minutesBefore: serializer.fromJson(json['minutesBefore']), - absoluteTime: serializer.fromJson(json['absoluteTime']), - enabled: serializer.fromJson(json['enabled']), - rawJson: serializer.fromJson(json['rawJson']), - ); - } + late final GeneratedColumn hasAttachments = GeneratedColumn( + 'has_attachments', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_attachments" IN (0, 1))', + ), + ); + static const VerificationMeta _providerMetadataJsonMeta = + const VerificationMeta('providerMetadataJson'); @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'calendarEventId': serializer.toJson(calendarEventId), - 'provider': serializer.toJson(provider), - 'method': serializer.toJson(method), - 'minutesBefore': serializer.toJson(minutesBefore), - 'absoluteTime': serializer.toJson(absoluteTime), - 'enabled': serializer.toJson(enabled), - 'rawJson': serializer.toJson(rawJson), - }; - } - - CalendarEventReminder copyWith({ - String? id, - String? calendarEventId, - String? provider, - Value method = const Value.absent(), - Value minutesBefore = const Value.absent(), - Value absoluteTime = const Value.absent(), - bool? enabled, - Value rawJson = const Value.absent(), - }) => CalendarEventReminder( - id: id ?? this.id, - calendarEventId: calendarEventId ?? this.calendarEventId, - provider: provider ?? this.provider, - method: method.present ? method.value : this.method, - minutesBefore: minutesBefore.present - ? minutesBefore.value - : this.minutesBefore, - absoluteTime: absoluteTime.present ? absoluteTime.value : this.absoluteTime, - enabled: enabled ?? this.enabled, - rawJson: rawJson.present ? rawJson.value : this.rawJson, - ); - CalendarEventReminder copyWithCompanion( - CalendarEventRemindersCompanion data, - ) { - return CalendarEventReminder( - id: data.id.present ? data.id.value : this.id, - calendarEventId: data.calendarEventId.present - ? data.calendarEventId.value - : this.calendarEventId, - provider: data.provider.present ? data.provider.value : this.provider, - method: data.method.present ? data.method.value : this.method, - minutesBefore: data.minutesBefore.present - ? data.minutesBefore.value - : this.minutesBefore, - absoluteTime: data.absoluteTime.present - ? data.absoluteTime.value - : this.absoluteTime, - enabled: data.enabled.present ? data.enabled.value : this.enabled, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - ); - } - - @override - String toString() { - return (StringBuffer('CalendarEventReminder(') - ..write('id: $id, ') - ..write('calendarEventId: $calendarEventId, ') - ..write('provider: $provider, ') - ..write('method: $method, ') - ..write('minutesBefore: $minutesBefore, ') - ..write('absoluteTime: $absoluteTime, ') - ..write('enabled: $enabled, ') - ..write('rawJson: $rawJson') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, - calendarEventId, - provider, - method, - minutesBefore, - absoluteTime, - enabled, - rawJson, + late final GeneratedColumn providerMetadataJson = + GeneratedColumn( + 'provider_metadata_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _deletedMeta = const VerificationMeta( + 'deleted', ); @override - bool operator ==(Object other) => - identical(this, other) || - (other is CalendarEventReminder && - other.id == this.id && - other.calendarEventId == this.calendarEventId && - other.provider == this.provider && - other.method == this.method && - other.minutesBefore == this.minutesBefore && - other.absoluteTime == this.absoluteTime && - other.enabled == this.enabled && - other.rawJson == this.rawJson); -} - -class CalendarEventRemindersCompanion - extends UpdateCompanion { - final Value id; - final Value calendarEventId; - final Value provider; - final Value method; - final Value minutesBefore; - final Value absoluteTime; - final Value enabled; - final Value rawJson; - final Value rowid; - const CalendarEventRemindersCompanion({ - this.id = const Value.absent(), - this.calendarEventId = const Value.absent(), - this.provider = const Value.absent(), - this.method = const Value.absent(), - this.minutesBefore = const Value.absent(), - this.absoluteTime = const Value.absent(), - this.enabled = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }); - CalendarEventRemindersCompanion.insert({ - required String id, - required String calendarEventId, - required String provider, - this.method = const Value.absent(), - this.minutesBefore = const Value.absent(), - this.absoluteTime = const Value.absent(), - this.enabled = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }) : id = Value(id), - calendarEventId = Value(calendarEventId), - provider = Value(provider); - static Insertable custom({ - Expression? id, - Expression? calendarEventId, - Expression? provider, - Expression? method, - Expression? minutesBefore, - Expression? absoluteTime, - Expression? enabled, - Expression? rawJson, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (calendarEventId != null) 'calendar_event_id': calendarEventId, - if (provider != null) 'provider': provider, - if (method != null) 'method': method, - if (minutesBefore != null) 'minutes_before': minutesBefore, - if (absoluteTime != null) 'absolute_time': absoluteTime, - if (enabled != null) 'enabled': enabled, - if (rawJson != null) 'raw_json': rawJson, - if (rowid != null) 'rowid': rowid, - }); - } - - CalendarEventRemindersCompanion copyWith({ - Value? id, - Value? calendarEventId, - Value? provider, - Value? method, - Value? minutesBefore, - Value? absoluteTime, - Value? enabled, - Value? rawJson, - Value? rowid, - }) { - return CalendarEventRemindersCompanion( - id: id ?? this.id, - calendarEventId: calendarEventId ?? this.calendarEventId, - provider: provider ?? this.provider, - method: method ?? this.method, - minutesBefore: minutesBefore ?? this.minutesBefore, - absoluteTime: absoluteTime ?? this.absoluteTime, - enabled: enabled ?? this.enabled, - rawJson: rawJson ?? this.rawJson, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (calendarEventId.present) { - map['calendar_event_id'] = Variable(calendarEventId.value); - } - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (method.present) { - map['method'] = Variable(method.value); - } - if (minutesBefore.present) { - map['minutes_before'] = Variable(minutesBefore.value); - } - if (absoluteTime.present) { - map['absolute_time'] = Variable(absoluteTime.value); - } - if (enabled.present) { - map['enabled'] = Variable(enabled.value); - } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - - @override - String toString() { - return (StringBuffer('CalendarEventRemindersCompanion(') - ..write('id: $id, ') - ..write('calendarEventId: $calendarEventId, ') - ..write('provider: $provider, ') - ..write('method: $method, ') - ..write('minutesBefore: $minutesBefore, ') - ..write('absoluteTime: $absoluteTime, ') - ..write('enabled: $enabled, ') - ..write('rawJson: $rawJson, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $CalendarSyncStatesTable extends CalendarSyncStates - with TableInfo<$CalendarSyncStatesTable, CalendarSyncState> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $CalendarSyncStatesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', + late final GeneratedColumn deleted = GeneratedColumn( + 'deleted', aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _accountIdMeta = const VerificationMeta( - 'accountId', + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted" IN (0, 1))', + ), ); + static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); @override - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', + late final GeneratedColumn hidden = GeneratedColumn( + 'hidden', aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES accounts (id) ON DELETE CASCADE', + 'CHECK ("hidden" IN (0, 1))', ), ); - static const VerificationMeta _calendarSourceIdMeta = const VerificationMeta( - 'calendarSourceId', + static const VerificationMeta _linksJsonMeta = const VerificationMeta( + 'linksJson', ); @override - late final GeneratedColumn calendarSourceId = GeneratedColumn( - 'calendar_source_id', + late final GeneratedColumn linksJson = GeneratedColumn( + 'links_json', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES calendar_sources (id) ON DELETE CASCADE', - ), ); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', + static const VerificationMeta _webViewLinkMeta = const VerificationMeta( + 'webViewLink', ); @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', + late final GeneratedColumn webViewLink = GeneratedColumn( + 'web_view_link', aliasedName, - false, + true, type: DriftSqlType.string, - requiredDuringInsert: true, + requiredDuringInsert: false, ); - static const VerificationMeta _syncKindMeta = const VerificationMeta( - 'syncKind', + static const VerificationMeta _assignmentInfoJsonMeta = + const VerificationMeta('assignmentInfoJson'); + @override + late final GeneratedColumn assignmentInfoJson = + GeneratedColumn( + 'assignment_info_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', ); @override - late final GeneratedColumn syncKind = GeneratedColumn( - 'sync_kind', + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _rangeStartMeta = const VerificationMeta( - 'rangeStart', + static const VerificationMeta _serverMissingMeta = const VerificationMeta( + 'serverMissing', ); @override - late final GeneratedColumn rangeStart = GeneratedColumn( - 'range_start', + late final GeneratedColumn serverMissing = GeneratedColumn( + 'server_missing', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("server_missing" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _rangeEndMeta = const VerificationMeta( - 'rangeEnd', + static const VerificationMeta _localDirtyMeta = const VerificationMeta( + 'localDirty', ); @override - late final GeneratedColumn rangeEnd = GeneratedColumn( - 'range_end', + late final GeneratedColumn localDirty = GeneratedColumn( + 'local_dirty', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("local_dirty" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _googleSyncTokenMeta = const VerificationMeta( - 'googleSyncToken', + static const VerificationMeta _pendingDeleteMeta = const VerificationMeta( + 'pendingDelete', ); @override - late final GeneratedColumn googleSyncToken = GeneratedColumn( - 'google_sync_token', + late final GeneratedColumn pendingDelete = GeneratedColumn( + 'pending_delete', aliasedName, - true, - type: DriftSqlType.string, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pending_delete" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _microsoftDeltaLinkMeta = - const VerificationMeta('microsoftDeltaLink'); - @override - late final GeneratedColumn microsoftDeltaLink = - GeneratedColumn( - 'microsoft_delta_link', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _lastFullSyncAtMeta = const VerificationMeta( - 'lastFullSyncAt', + static const VerificationMeta _pendingMoveMeta = const VerificationMeta( + 'pendingMove', ); @override - late final GeneratedColumn lastFullSyncAt = GeneratedColumn( - 'last_full_sync_at', + late final GeneratedColumn pendingMove = GeneratedColumn( + 'pending_move', aliasedName, - true, - type: DriftSqlType.int, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("pending_move" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _localCreatedMeta = const VerificationMeta( + 'localCreated', ); - static const VerificationMeta _lastIncrementalSyncAtMeta = - const VerificationMeta('lastIncrementalSyncAt'); @override - late final GeneratedColumn lastIncrementalSyncAt = GeneratedColumn( - 'last_incremental_sync_at', + late final GeneratedColumn localCreated = GeneratedColumn( + 'local_created', aliasedName, - true, - type: DriftSqlType.int, + false, + type: DriftSqlType.bool, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("local_created" IN (0, 1))', + ), + defaultValue: const Constant(false), ); - static const VerificationMeta _lastErrorMeta = const VerificationMeta( - 'lastError', + static const VerificationMeta _syncBaseUpdatedUtcMeta = + const VerificationMeta('syncBaseUpdatedUtc'); + @override + late final GeneratedColumn syncBaseUpdatedUtc = + GeneratedColumn( + 'sync_base_updated_utc', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastSyncedAtUtcMeta = const VerificationMeta( + 'lastSyncedAtUtc', ); @override - late final GeneratedColumn lastError = GeneratedColumn( - 'last_error', + late final GeneratedColumn lastSyncedAtUtc = GeneratedColumn( + 'last_synced_at_utc', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, ); - static const VerificationMeta _rawStateJsonMeta = const VerificationMeta( - 'rawStateJson', + static const VerificationMeta _createdLocalAtUtcMeta = const VerificationMeta( + 'createdLocalAtUtc', ); @override - late final GeneratedColumn rawStateJson = GeneratedColumn( - 'raw_state_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, + late final GeneratedColumn createdLocalAtUtc = + GeneratedColumn( + 'created_local_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedLocalAtUtcMeta = const VerificationMeta( + 'updatedLocalAtUtc', ); @override + late final GeneratedColumn updatedLocalAtUtc = + GeneratedColumn( + 'updated_local_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override List get $columns => [ - id, accountId, - calendarSourceId, - provider, - syncKind, - rangeStart, - rangeEnd, - googleSyncToken, - microsoftDeltaLink, - lastFullSyncAt, - lastIncrementalSyncAt, - lastError, - rawStateJson, + taskListId, + id, + davCollectionId, + davObjectId, + davComponentId, + icalUid, + recurrenceIdKey, + icalPriority, + percentComplete, + taskLocation, + taskUrl, + taskClassification, + taskPinned, + taskHideSubtasks, + taskHideCompletedSubtasks, + taskAlarmsJson, + parentUid, + sortOrder, + providerExtensionProjectionJson, + projectionVersion, + kind, + etag, + title, + updatedUtc, + selfLink, + parent, + position, + notes, + status, + dueUtc, + completedUtc, + providerStatus, + bodyContent, + bodyContentType, + microsoftDueDateTime, + microsoftDueTimeZone, + microsoftStartDateTime, + microsoftStartTimeZone, + microsoftReminderDateTime, + microsoftReminderTimeZone, + microsoftIsReminderOn, + microsoftCompletedDateTime, + microsoftCompletedTimeZone, + microsoftChecklistItemsJson, + recurrenceJson, + importance, + categoriesJson, + hasAttachments, + providerMetadataJson, + deleted, + hidden, + linksJson, + webViewLink, + assignmentInfoJson, + rawJson, + serverMissing, + localDirty, + pendingDelete, + pendingMove, + localCreated, + syncBaseUpdatedUtc, + lastSyncedAtUtc, + createdLocalAtUtc, + updatedLocalAtUtc, ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'calendar_sync_states'; + static const String $name = 'tasks'; @override VerificationContext validateIntegrity( - Insertable instance, { + Insertable instance, { bool isInserting = false, }) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } if (data.containsKey('account_id')) { context.handle( _accountIdMeta, @@ -11684,3303 +8813,19481 @@ class $CalendarSyncStatesTable extends CalendarSyncStates } else if (isInserting) { context.missing(_accountIdMeta); } - if (data.containsKey('calendar_source_id')) { + if (data.containsKey('task_list_id')) { context.handle( - _calendarSourceIdMeta, - calendarSourceId.isAcceptableOrUnknown( - data['calendar_source_id']!, - _calendarSourceIdMeta, + _taskListIdMeta, + taskListId.isAcceptableOrUnknown( + data['task_list_id']!, + _taskListIdMeta, ), ); + } else if (isInserting) { + context.missing(_taskListIdMeta); } - if (data.containsKey('provider')) { + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('dav_collection_id')) { context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), ); - } else if (isInserting) { - context.missing(_providerMeta); } - if (data.containsKey('sync_kind')) { + if (data.containsKey('dav_object_id')) { context.handle( - _syncKindMeta, - syncKind.isAcceptableOrUnknown(data['sync_kind']!, _syncKindMeta), + _davObjectIdMeta, + davObjectId.isAcceptableOrUnknown( + data['dav_object_id']!, + _davObjectIdMeta, + ), ); - } else if (isInserting) { - context.missing(_syncKindMeta); } - if (data.containsKey('range_start')) { + if (data.containsKey('dav_component_id')) { context.handle( - _rangeStartMeta, - rangeStart.isAcceptableOrUnknown(data['range_start']!, _rangeStartMeta), + _davComponentIdMeta, + davComponentId.isAcceptableOrUnknown( + data['dav_component_id']!, + _davComponentIdMeta, + ), ); } - if (data.containsKey('range_end')) { + if (data.containsKey('ical_uid')) { context.handle( - _rangeEndMeta, - rangeEnd.isAcceptableOrUnknown(data['range_end']!, _rangeEndMeta), + _icalUidMeta, + icalUid.isAcceptableOrUnknown(data['ical_uid']!, _icalUidMeta), ); } - if (data.containsKey('google_sync_token')) { + if (data.containsKey('recurrence_id_key')) { context.handle( - _googleSyncTokenMeta, - googleSyncToken.isAcceptableOrUnknown( - data['google_sync_token']!, - _googleSyncTokenMeta, + _recurrenceIdKeyMeta, + recurrenceIdKey.isAcceptableOrUnknown( + data['recurrence_id_key']!, + _recurrenceIdKeyMeta, ), ); } - if (data.containsKey('microsoft_delta_link')) { + if (data.containsKey('ical_priority')) { context.handle( - _microsoftDeltaLinkMeta, - microsoftDeltaLink.isAcceptableOrUnknown( - data['microsoft_delta_link']!, - _microsoftDeltaLinkMeta, + _icalPriorityMeta, + icalPriority.isAcceptableOrUnknown( + data['ical_priority']!, + _icalPriorityMeta, ), ); } - if (data.containsKey('last_full_sync_at')) { + if (data.containsKey('percent_complete')) { context.handle( - _lastFullSyncAtMeta, - lastFullSyncAt.isAcceptableOrUnknown( - data['last_full_sync_at']!, - _lastFullSyncAtMeta, + _percentCompleteMeta, + percentComplete.isAcceptableOrUnknown( + data['percent_complete']!, + _percentCompleteMeta, ), ); } - if (data.containsKey('last_incremental_sync_at')) { + if (data.containsKey('task_location')) { context.handle( - _lastIncrementalSyncAtMeta, - lastIncrementalSyncAt.isAcceptableOrUnknown( - data['last_incremental_sync_at']!, - _lastIncrementalSyncAtMeta, + _taskLocationMeta, + taskLocation.isAcceptableOrUnknown( + data['task_location']!, + _taskLocationMeta, ), ); } - if (data.containsKey('last_error')) { + if (data.containsKey('task_url')) { context.handle( - _lastErrorMeta, - lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + _taskUrlMeta, + taskUrl.isAcceptableOrUnknown(data['task_url']!, _taskUrlMeta), ); } - if (data.containsKey('raw_state_json')) { + if (data.containsKey('task_classification')) { context.handle( - _rawStateJsonMeta, - rawStateJson.isAcceptableOrUnknown( - data['raw_state_json']!, - _rawStateJsonMeta, + _taskClassificationMeta, + taskClassification.isAcceptableOrUnknown( + data['task_classification']!, + _taskClassificationMeta, ), ); } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - CalendarSyncState map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarSyncState( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, - accountId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}account_id'], - )!, - calendarSourceId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}calendar_source_id'], - ), - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - )!, - syncKind: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}sync_kind'], - )!, - rangeStart: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}range_start'], - ), - rangeEnd: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}range_end'], - ), - googleSyncToken: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}google_sync_token'], - ), - microsoftDeltaLink: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}microsoft_delta_link'], - ), - lastFullSyncAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}last_full_sync_at'], - ), - lastIncrementalSyncAt: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}last_incremental_sync_at'], - ), - lastError: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}last_error'], - ), - rawStateJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_state_json'], - ), - ); - } - - @override - $CalendarSyncStatesTable createAlias(String alias) { - return $CalendarSyncStatesTable(attachedDatabase, alias); - } -} - -class CalendarSyncState extends DataClass - implements Insertable { - final String id; - final String accountId; - final String? calendarSourceId; - final String provider; - final String syncKind; - final String? rangeStart; - final String? rangeEnd; - final String? googleSyncToken; - final String? microsoftDeltaLink; - final int? lastFullSyncAt; - final int? lastIncrementalSyncAt; - final String? lastError; - final String? rawStateJson; - const CalendarSyncState({ - required this.id, - required this.accountId, - this.calendarSourceId, - required this.provider, - required this.syncKind, - this.rangeStart, - this.rangeEnd, - this.googleSyncToken, - this.microsoftDeltaLink, - this.lastFullSyncAt, - this.lastIncrementalSyncAt, - this.lastError, - this.rawStateJson, - }); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - if (!nullToAbsent || calendarSourceId != null) { - map['calendar_source_id'] = Variable(calendarSourceId); + if (data.containsKey('task_pinned')) { + context.handle( + _taskPinnedMeta, + taskPinned.isAcceptableOrUnknown(data['task_pinned']!, _taskPinnedMeta), + ); } - map['provider'] = Variable(provider); - map['sync_kind'] = Variable(syncKind); - if (!nullToAbsent || rangeStart != null) { - map['range_start'] = Variable(rangeStart); + if (data.containsKey('task_hide_subtasks')) { + context.handle( + _taskHideSubtasksMeta, + taskHideSubtasks.isAcceptableOrUnknown( + data['task_hide_subtasks']!, + _taskHideSubtasksMeta, + ), + ); } - if (!nullToAbsent || rangeEnd != null) { - map['range_end'] = Variable(rangeEnd); + if (data.containsKey('task_hide_completed_subtasks')) { + context.handle( + _taskHideCompletedSubtasksMeta, + taskHideCompletedSubtasks.isAcceptableOrUnknown( + data['task_hide_completed_subtasks']!, + _taskHideCompletedSubtasksMeta, + ), + ); } - if (!nullToAbsent || googleSyncToken != null) { - map['google_sync_token'] = Variable(googleSyncToken); + if (data.containsKey('task_alarms_json')) { + context.handle( + _taskAlarmsJsonMeta, + taskAlarmsJson.isAcceptableOrUnknown( + data['task_alarms_json']!, + _taskAlarmsJsonMeta, + ), + ); } - if (!nullToAbsent || microsoftDeltaLink != null) { - map['microsoft_delta_link'] = Variable(microsoftDeltaLink); + if (data.containsKey('parent_uid')) { + context.handle( + _parentUidMeta, + parentUid.isAcceptableOrUnknown(data['parent_uid']!, _parentUidMeta), + ); } - if (!nullToAbsent || lastFullSyncAt != null) { - map['last_full_sync_at'] = Variable(lastFullSyncAt); + if (data.containsKey('sort_order')) { + context.handle( + _sortOrderMeta, + sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta), + ); } - if (!nullToAbsent || lastIncrementalSyncAt != null) { - map['last_incremental_sync_at'] = Variable(lastIncrementalSyncAt); + if (data.containsKey('provider_extension_projection_json')) { + context.handle( + _providerExtensionProjectionJsonMeta, + providerExtensionProjectionJson.isAcceptableOrUnknown( + data['provider_extension_projection_json']!, + _providerExtensionProjectionJsonMeta, + ), + ); } - if (!nullToAbsent || lastError != null) { - map['last_error'] = Variable(lastError); + if (data.containsKey('projection_version')) { + context.handle( + _projectionVersionMeta, + projectionVersion.isAcceptableOrUnknown( + data['projection_version']!, + _projectionVersionMeta, + ), + ); } - if (!nullToAbsent || rawStateJson != null) { - map['raw_state_json'] = Variable(rawStateJson); + if (data.containsKey('kind')) { + context.handle( + _kindMeta, + kind.isAcceptableOrUnknown(data['kind']!, _kindMeta), + ); } - return map; - } - - CalendarSyncStatesCompanion toCompanion(bool nullToAbsent) { - return CalendarSyncStatesCompanion( - id: Value(id), - accountId: Value(accountId), - calendarSourceId: calendarSourceId == null && nullToAbsent - ? const Value.absent() - : Value(calendarSourceId), - provider: Value(provider), - syncKind: Value(syncKind), - rangeStart: rangeStart == null && nullToAbsent - ? const Value.absent() - : Value(rangeStart), - rangeEnd: rangeEnd == null && nullToAbsent - ? const Value.absent() - : Value(rangeEnd), - googleSyncToken: googleSyncToken == null && nullToAbsent - ? const Value.absent() - : Value(googleSyncToken), - microsoftDeltaLink: microsoftDeltaLink == null && nullToAbsent - ? const Value.absent() - : Value(microsoftDeltaLink), - lastFullSyncAt: lastFullSyncAt == null && nullToAbsent - ? const Value.absent() - : Value(lastFullSyncAt), - lastIncrementalSyncAt: lastIncrementalSyncAt == null && nullToAbsent - ? const Value.absent() - : Value(lastIncrementalSyncAt), - lastError: lastError == null && nullToAbsent - ? const Value.absent() - : Value(lastError), - rawStateJson: rawStateJson == null && nullToAbsent - ? const Value.absent() - : Value(rawStateJson), - ); - } - - factory CalendarSyncState.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarSyncState( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - calendarSourceId: serializer.fromJson(json['calendarSourceId']), - provider: serializer.fromJson(json['provider']), - syncKind: serializer.fromJson(json['syncKind']), - rangeStart: serializer.fromJson(json['rangeStart']), - rangeEnd: serializer.fromJson(json['rangeEnd']), - googleSyncToken: serializer.fromJson(json['googleSyncToken']), - microsoftDeltaLink: serializer.fromJson( - json['microsoftDeltaLink'], - ), - lastFullSyncAt: serializer.fromJson(json['lastFullSyncAt']), - lastIncrementalSyncAt: serializer.fromJson( - json['lastIncrementalSyncAt'], - ), - lastError: serializer.fromJson(json['lastError']), - rawStateJson: serializer.fromJson(json['rawStateJson']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'calendarSourceId': serializer.toJson(calendarSourceId), - 'provider': serializer.toJson(provider), - 'syncKind': serializer.toJson(syncKind), - 'rangeStart': serializer.toJson(rangeStart), - 'rangeEnd': serializer.toJson(rangeEnd), - 'googleSyncToken': serializer.toJson(googleSyncToken), - 'microsoftDeltaLink': serializer.toJson(microsoftDeltaLink), - 'lastFullSyncAt': serializer.toJson(lastFullSyncAt), - 'lastIncrementalSyncAt': serializer.toJson(lastIncrementalSyncAt), - 'lastError': serializer.toJson(lastError), - 'rawStateJson': serializer.toJson(rawStateJson), - }; - } - - CalendarSyncState copyWith({ - String? id, - String? accountId, - Value calendarSourceId = const Value.absent(), - String? provider, - String? syncKind, - Value rangeStart = const Value.absent(), - Value rangeEnd = const Value.absent(), - Value googleSyncToken = const Value.absent(), - Value microsoftDeltaLink = const Value.absent(), - Value lastFullSyncAt = const Value.absent(), - Value lastIncrementalSyncAt = const Value.absent(), - Value lastError = const Value.absent(), - Value rawStateJson = const Value.absent(), - }) => CalendarSyncState( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - calendarSourceId: calendarSourceId.present - ? calendarSourceId.value - : this.calendarSourceId, - provider: provider ?? this.provider, - syncKind: syncKind ?? this.syncKind, - rangeStart: rangeStart.present ? rangeStart.value : this.rangeStart, - rangeEnd: rangeEnd.present ? rangeEnd.value : this.rangeEnd, - googleSyncToken: googleSyncToken.present - ? googleSyncToken.value - : this.googleSyncToken, - microsoftDeltaLink: microsoftDeltaLink.present - ? microsoftDeltaLink.value - : this.microsoftDeltaLink, - lastFullSyncAt: lastFullSyncAt.present - ? lastFullSyncAt.value - : this.lastFullSyncAt, - lastIncrementalSyncAt: lastIncrementalSyncAt.present - ? lastIncrementalSyncAt.value - : this.lastIncrementalSyncAt, - lastError: lastError.present ? lastError.value : this.lastError, - rawStateJson: rawStateJson.present ? rawStateJson.value : this.rawStateJson, - ); - CalendarSyncState copyWithCompanion(CalendarSyncStatesCompanion data) { - return CalendarSyncState( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - calendarSourceId: data.calendarSourceId.present - ? data.calendarSourceId.value - : this.calendarSourceId, - provider: data.provider.present ? data.provider.value : this.provider, - syncKind: data.syncKind.present ? data.syncKind.value : this.syncKind, - rangeStart: data.rangeStart.present - ? data.rangeStart.value - : this.rangeStart, - rangeEnd: data.rangeEnd.present ? data.rangeEnd.value : this.rangeEnd, - googleSyncToken: data.googleSyncToken.present - ? data.googleSyncToken.value - : this.googleSyncToken, - microsoftDeltaLink: data.microsoftDeltaLink.present - ? data.microsoftDeltaLink.value - : this.microsoftDeltaLink, - lastFullSyncAt: data.lastFullSyncAt.present - ? data.lastFullSyncAt.value - : this.lastFullSyncAt, - lastIncrementalSyncAt: data.lastIncrementalSyncAt.present - ? data.lastIncrementalSyncAt.value - : this.lastIncrementalSyncAt, - lastError: data.lastError.present ? data.lastError.value : this.lastError, - rawStateJson: data.rawStateJson.present - ? data.rawStateJson.value - : this.rawStateJson, - ); - } - - @override - String toString() { - return (StringBuffer('CalendarSyncState(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('provider: $provider, ') - ..write('syncKind: $syncKind, ') - ..write('rangeStart: $rangeStart, ') - ..write('rangeEnd: $rangeEnd, ') - ..write('googleSyncToken: $googleSyncToken, ') - ..write('microsoftDeltaLink: $microsoftDeltaLink, ') - ..write('lastFullSyncAt: $lastFullSyncAt, ') - ..write('lastIncrementalSyncAt: $lastIncrementalSyncAt, ') - ..write('lastError: $lastError, ') - ..write('rawStateJson: $rawStateJson') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, - accountId, - calendarSourceId, - provider, - syncKind, - rangeStart, - rangeEnd, - googleSyncToken, - microsoftDeltaLink, - lastFullSyncAt, - lastIncrementalSyncAt, - lastError, - rawStateJson, - ); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CalendarSyncState && - other.id == this.id && - other.accountId == this.accountId && - other.calendarSourceId == this.calendarSourceId && - other.provider == this.provider && - other.syncKind == this.syncKind && - other.rangeStart == this.rangeStart && - other.rangeEnd == this.rangeEnd && - other.googleSyncToken == this.googleSyncToken && - other.microsoftDeltaLink == this.microsoftDeltaLink && - other.lastFullSyncAt == this.lastFullSyncAt && - other.lastIncrementalSyncAt == this.lastIncrementalSyncAt && - other.lastError == this.lastError && - other.rawStateJson == this.rawStateJson); -} - -class CalendarSyncStatesCompanion extends UpdateCompanion { - final Value id; - final Value accountId; - final Value calendarSourceId; - final Value provider; - final Value syncKind; - final Value rangeStart; - final Value rangeEnd; - final Value googleSyncToken; - final Value microsoftDeltaLink; - final Value lastFullSyncAt; - final Value lastIncrementalSyncAt; - final Value lastError; - final Value rawStateJson; - final Value rowid; - const CalendarSyncStatesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.calendarSourceId = const Value.absent(), - this.provider = const Value.absent(), - this.syncKind = const Value.absent(), - this.rangeStart = const Value.absent(), - this.rangeEnd = const Value.absent(), - this.googleSyncToken = const Value.absent(), - this.microsoftDeltaLink = const Value.absent(), - this.lastFullSyncAt = const Value.absent(), - this.lastIncrementalSyncAt = const Value.absent(), - this.lastError = const Value.absent(), - this.rawStateJson = const Value.absent(), - this.rowid = const Value.absent(), - }); - CalendarSyncStatesCompanion.insert({ - required String id, - required String accountId, - this.calendarSourceId = const Value.absent(), - required String provider, - required String syncKind, - this.rangeStart = const Value.absent(), - this.rangeEnd = const Value.absent(), - this.googleSyncToken = const Value.absent(), - this.microsoftDeltaLink = const Value.absent(), - this.lastFullSyncAt = const Value.absent(), - this.lastIncrementalSyncAt = const Value.absent(), - this.lastError = const Value.absent(), - this.rawStateJson = const Value.absent(), - this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - provider = Value(provider), - syncKind = Value(syncKind); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? calendarSourceId, - Expression? provider, - Expression? syncKind, - Expression? rangeStart, - Expression? rangeEnd, - Expression? googleSyncToken, - Expression? microsoftDeltaLink, - Expression? lastFullSyncAt, - Expression? lastIncrementalSyncAt, - Expression? lastError, - Expression? rawStateJson, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (calendarSourceId != null) 'calendar_source_id': calendarSourceId, - if (provider != null) 'provider': provider, - if (syncKind != null) 'sync_kind': syncKind, - if (rangeStart != null) 'range_start': rangeStart, - if (rangeEnd != null) 'range_end': rangeEnd, - if (googleSyncToken != null) 'google_sync_token': googleSyncToken, - if (microsoftDeltaLink != null) - 'microsoft_delta_link': microsoftDeltaLink, - if (lastFullSyncAt != null) 'last_full_sync_at': lastFullSyncAt, - if (lastIncrementalSyncAt != null) - 'last_incremental_sync_at': lastIncrementalSyncAt, - if (lastError != null) 'last_error': lastError, - if (rawStateJson != null) 'raw_state_json': rawStateJson, - if (rowid != null) 'rowid': rowid, - }); - } - - CalendarSyncStatesCompanion copyWith({ - Value? id, - Value? accountId, - Value? calendarSourceId, - Value? provider, - Value? syncKind, - Value? rangeStart, - Value? rangeEnd, - Value? googleSyncToken, - Value? microsoftDeltaLink, - Value? lastFullSyncAt, - Value? lastIncrementalSyncAt, - Value? lastError, - Value? rawStateJson, - Value? rowid, - }) { - return CalendarSyncStatesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - calendarSourceId: calendarSourceId ?? this.calendarSourceId, - provider: provider ?? this.provider, - syncKind: syncKind ?? this.syncKind, - rangeStart: rangeStart ?? this.rangeStart, - rangeEnd: rangeEnd ?? this.rangeEnd, - googleSyncToken: googleSyncToken ?? this.googleSyncToken, - microsoftDeltaLink: microsoftDeltaLink ?? this.microsoftDeltaLink, - lastFullSyncAt: lastFullSyncAt ?? this.lastFullSyncAt, - lastIncrementalSyncAt: - lastIncrementalSyncAt ?? this.lastIncrementalSyncAt, - lastError: lastError ?? this.lastError, - rawStateJson: rawStateJson ?? this.rawStateJson, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (calendarSourceId.present) { - map['calendar_source_id'] = Variable(calendarSourceId.value); - } - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (syncKind.present) { - map['sync_kind'] = Variable(syncKind.value); - } - if (rangeStart.present) { - map['range_start'] = Variable(rangeStart.value); - } - if (rangeEnd.present) { - map['range_end'] = Variable(rangeEnd.value); - } - if (googleSyncToken.present) { - map['google_sync_token'] = Variable(googleSyncToken.value); - } - if (microsoftDeltaLink.present) { - map['microsoft_delta_link'] = Variable(microsoftDeltaLink.value); + if (data.containsKey('etag')) { + context.handle( + _etagMeta, + etag.isAcceptableOrUnknown(data['etag']!, _etagMeta), + ); } - if (lastFullSyncAt.present) { - map['last_full_sync_at'] = Variable(lastFullSyncAt.value); + if (data.containsKey('title')) { + context.handle( + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), + ); + } else if (isInserting) { + context.missing(_titleMeta); } - if (lastIncrementalSyncAt.present) { - map['last_incremental_sync_at'] = Variable( - lastIncrementalSyncAt.value, + if (data.containsKey('updated_utc')) { + context.handle( + _updatedUtcMeta, + updatedUtc.isAcceptableOrUnknown(data['updated_utc']!, _updatedUtcMeta), ); } - if (lastError.present) { - map['last_error'] = Variable(lastError.value); + if (data.containsKey('self_link')) { + context.handle( + _selfLinkMeta, + selfLink.isAcceptableOrUnknown(data['self_link']!, _selfLinkMeta), + ); } - if (rawStateJson.present) { - map['raw_state_json'] = Variable(rawStateJson.value); + if (data.containsKey('parent')) { + context.handle( + _parentMeta, + parent.isAcceptableOrUnknown(data['parent']!, _parentMeta), + ); } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); + if (data.containsKey('position')) { + context.handle( + _positionMeta, + position.isAcceptableOrUnknown(data['position']!, _positionMeta), + ); } - return map; - } - - @override - String toString() { - return (StringBuffer('CalendarSyncStatesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('calendarSourceId: $calendarSourceId, ') - ..write('provider: $provider, ') - ..write('syncKind: $syncKind, ') - ..write('rangeStart: $rangeStart, ') - ..write('rangeEnd: $rangeEnd, ') - ..write('googleSyncToken: $googleSyncToken, ') - ..write('microsoftDeltaLink: $microsoftDeltaLink, ') - ..write('lastFullSyncAt: $lastFullSyncAt, ') - ..write('lastIncrementalSyncAt: $lastIncrementalSyncAt, ') - ..write('lastError: $lastError, ') - ..write('rawStateJson: $rawStateJson, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $CalendarColorsTable extends CalendarColors - with TableInfo<$CalendarColorsTable, CalendarColor> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $CalendarColorsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _providerMeta = const VerificationMeta( - 'provider', - ); - @override - late final GeneratedColumn provider = GeneratedColumn( - 'provider', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _colorTypeMeta = const VerificationMeta( - 'colorType', - ); - @override - late final GeneratedColumn colorType = GeneratedColumn( - 'color_type', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _colorIdMeta = const VerificationMeta( - 'colorId', - ); - @override - late final GeneratedColumn colorId = GeneratedColumn( - 'color_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _backgroundMeta = const VerificationMeta( - 'background', - ); - @override - late final GeneratedColumn background = GeneratedColumn( - 'background', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _foregroundMeta = const VerificationMeta( - 'foreground', - ); - @override - late final GeneratedColumn foreground = GeneratedColumn( - 'foreground', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _rawJsonMeta = const VerificationMeta( - 'rawJson', - ); - @override - late final GeneratedColumn rawJson = GeneratedColumn( - 'raw_json', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - @override - List get $columns => [ - provider, - colorType, - colorId, - background, - foreground, - rawJson, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'calendar_colors'; - @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('provider')) { + if (data.containsKey('notes')) { context.handle( - _providerMeta, - provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), ); - } else if (isInserting) { - context.missing(_providerMeta); } - if (data.containsKey('color_type')) { + if (data.containsKey('status')) { context.handle( - _colorTypeMeta, - colorType.isAcceptableOrUnknown(data['color_type']!, _colorTypeMeta), + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), ); - } else if (isInserting) { - context.missing(_colorTypeMeta); } - if (data.containsKey('color_id')) { + if (data.containsKey('due_utc')) { context.handle( - _colorIdMeta, - colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), + _dueUtcMeta, + dueUtc.isAcceptableOrUnknown(data['due_utc']!, _dueUtcMeta), ); - } else if (isInserting) { - context.missing(_colorIdMeta); } - if (data.containsKey('background')) { + if (data.containsKey('completed_utc')) { context.handle( - _backgroundMeta, - background.isAcceptableOrUnknown(data['background']!, _backgroundMeta), + _completedUtcMeta, + completedUtc.isAcceptableOrUnknown( + data['completed_utc']!, + _completedUtcMeta, + ), ); - } else if (isInserting) { - context.missing(_backgroundMeta); } - if (data.containsKey('foreground')) { + if (data.containsKey('provider_status')) { context.handle( - _foregroundMeta, - foreground.isAcceptableOrUnknown(data['foreground']!, _foregroundMeta), + _providerStatusMeta, + providerStatus.isAcceptableOrUnknown( + data['provider_status']!, + _providerStatusMeta, + ), ); } - if (data.containsKey('raw_json')) { + if (data.containsKey('body_content')) { context.handle( - _rawJsonMeta, - rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + _bodyContentMeta, + bodyContent.isAcceptableOrUnknown( + data['body_content']!, + _bodyContentMeta, + ), ); } - return context; - } - - @override - Set get $primaryKey => {provider, colorType, colorId}; - @override - CalendarColor map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return CalendarColor( - provider: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}provider'], - )!, - colorType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}color_type'], - )!, - colorId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}color_id'], - )!, - background: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}background'], - )!, - foreground: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}foreground'], - ), - rawJson: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}raw_json'], - ), - ); - } - - @override - $CalendarColorsTable createAlias(String alias) { - return $CalendarColorsTable(attachedDatabase, alias); - } -} - -class CalendarColor extends DataClass implements Insertable { - final String provider; - final String colorType; - final String colorId; - final String background; - final String? foreground; - final String? rawJson; - const CalendarColor({ - required this.provider, - required this.colorType, - required this.colorId, - required this.background, - this.foreground, - this.rawJson, - }); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['provider'] = Variable(provider); - map['color_type'] = Variable(colorType); - map['color_id'] = Variable(colorId); - map['background'] = Variable(background); - if (!nullToAbsent || foreground != null) { - map['foreground'] = Variable(foreground); + if (data.containsKey('body_content_type')) { + context.handle( + _bodyContentTypeMeta, + bodyContentType.isAcceptableOrUnknown( + data['body_content_type']!, + _bodyContentTypeMeta, + ), + ); } - if (!nullToAbsent || rawJson != null) { - map['raw_json'] = Variable(rawJson); + if (data.containsKey('microsoft_due_date_time')) { + context.handle( + _microsoftDueDateTimeMeta, + microsoftDueDateTime.isAcceptableOrUnknown( + data['microsoft_due_date_time']!, + _microsoftDueDateTimeMeta, + ), + ); } - return map; - } - - CalendarColorsCompanion toCompanion(bool nullToAbsent) { - return CalendarColorsCompanion( - provider: Value(provider), - colorType: Value(colorType), - colorId: Value(colorId), - background: Value(background), - foreground: foreground == null && nullToAbsent - ? const Value.absent() - : Value(foreground), - rawJson: rawJson == null && nullToAbsent - ? const Value.absent() - : Value(rawJson), - ); - } - - factory CalendarColor.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return CalendarColor( - provider: serializer.fromJson(json['provider']), - colorType: serializer.fromJson(json['colorType']), - colorId: serializer.fromJson(json['colorId']), - background: serializer.fromJson(json['background']), - foreground: serializer.fromJson(json['foreground']), - rawJson: serializer.fromJson(json['rawJson']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'provider': serializer.toJson(provider), - 'colorType': serializer.toJson(colorType), - 'colorId': serializer.toJson(colorId), - 'background': serializer.toJson(background), - 'foreground': serializer.toJson(foreground), - 'rawJson': serializer.toJson(rawJson), - }; - } - - CalendarColor copyWith({ - String? provider, - String? colorType, - String? colorId, - String? background, - Value foreground = const Value.absent(), - Value rawJson = const Value.absent(), - }) => CalendarColor( - provider: provider ?? this.provider, - colorType: colorType ?? this.colorType, - colorId: colorId ?? this.colorId, - background: background ?? this.background, - foreground: foreground.present ? foreground.value : this.foreground, - rawJson: rawJson.present ? rawJson.value : this.rawJson, - ); - CalendarColor copyWithCompanion(CalendarColorsCompanion data) { - return CalendarColor( - provider: data.provider.present ? data.provider.value : this.provider, - colorType: data.colorType.present ? data.colorType.value : this.colorType, - colorId: data.colorId.present ? data.colorId.value : this.colorId, - background: data.background.present - ? data.background.value - : this.background, - foreground: data.foreground.present - ? data.foreground.value - : this.foreground, - rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, - ); - } - - @override - String toString() { - return (StringBuffer('CalendarColor(') - ..write('provider: $provider, ') - ..write('colorType: $colorType, ') - ..write('colorId: $colorId, ') - ..write('background: $background, ') - ..write('foreground: $foreground, ') - ..write('rawJson: $rawJson') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - provider, - colorType, - colorId, - background, - foreground, - rawJson, - ); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is CalendarColor && - other.provider == this.provider && - other.colorType == this.colorType && - other.colorId == this.colorId && - other.background == this.background && - other.foreground == this.foreground && - other.rawJson == this.rawJson); -} - -class CalendarColorsCompanion extends UpdateCompanion { - final Value provider; - final Value colorType; - final Value colorId; - final Value background; - final Value foreground; - final Value rawJson; - final Value rowid; - const CalendarColorsCompanion({ - this.provider = const Value.absent(), - this.colorType = const Value.absent(), - this.colorId = const Value.absent(), - this.background = const Value.absent(), - this.foreground = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }); - CalendarColorsCompanion.insert({ - required String provider, - required String colorType, - required String colorId, - required String background, - this.foreground = const Value.absent(), - this.rawJson = const Value.absent(), - this.rowid = const Value.absent(), - }) : provider = Value(provider), - colorType = Value(colorType), - colorId = Value(colorId), - background = Value(background); - static Insertable custom({ - Expression? provider, - Expression? colorType, - Expression? colorId, - Expression? background, - Expression? foreground, - Expression? rawJson, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (provider != null) 'provider': provider, - if (colorType != null) 'color_type': colorType, - if (colorId != null) 'color_id': colorId, - if (background != null) 'background': background, - if (foreground != null) 'foreground': foreground, - if (rawJson != null) 'raw_json': rawJson, - if (rowid != null) 'rowid': rowid, - }); - } - - CalendarColorsCompanion copyWith({ - Value? provider, - Value? colorType, - Value? colorId, - Value? background, - Value? foreground, - Value? rawJson, - Value? rowid, - }) { - return CalendarColorsCompanion( - provider: provider ?? this.provider, - colorType: colorType ?? this.colorType, - colorId: colorId ?? this.colorId, - background: background ?? this.background, - foreground: foreground ?? this.foreground, - rawJson: rawJson ?? this.rawJson, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (provider.present) { - map['provider'] = Variable(provider.value); - } - if (colorType.present) { - map['color_type'] = Variable(colorType.value); - } - if (colorId.present) { - map['color_id'] = Variable(colorId.value); - } - if (background.present) { - map['background'] = Variable(background.value); - } - if (foreground.present) { - map['foreground'] = Variable(foreground.value); + if (data.containsKey('microsoft_due_time_zone')) { + context.handle( + _microsoftDueTimeZoneMeta, + microsoftDueTimeZone.isAcceptableOrUnknown( + data['microsoft_due_time_zone']!, + _microsoftDueTimeZoneMeta, + ), + ); } - if (rawJson.present) { - map['raw_json'] = Variable(rawJson.value); + if (data.containsKey('microsoft_start_date_time')) { + context.handle( + _microsoftStartDateTimeMeta, + microsoftStartDateTime.isAcceptableOrUnknown( + data['microsoft_start_date_time']!, + _microsoftStartDateTimeMeta, + ), + ); } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); + if (data.containsKey('microsoft_start_time_zone')) { + context.handle( + _microsoftStartTimeZoneMeta, + microsoftStartTimeZone.isAcceptableOrUnknown( + data['microsoft_start_time_zone']!, + _microsoftStartTimeZoneMeta, + ), + ); } - return map; - } - - @override - String toString() { - return (StringBuffer('CalendarColorsCompanion(') - ..write('provider: $provider, ') - ..write('colorType: $colorType, ') - ..write('colorId: $colorId, ') - ..write('background: $background, ') - ..write('foreground: $foreground, ') - ..write('rawJson: $rawJson, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $ScheduleItemOverridesTable extends ScheduleItemOverrides - with TableInfo<$ScheduleItemOverridesTable, ScheduleItemOverride> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ScheduleItemOverridesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _accountIdMeta = const VerificationMeta( - 'accountId', - ); - @override - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES accounts (id) ON DELETE CASCADE', - ), - ); - static const VerificationMeta _sourceTypeMeta = const VerificationMeta( - 'sourceType', - ); - @override - late final GeneratedColumn sourceType = GeneratedColumn( - 'source_type', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _sourceIdMeta = const VerificationMeta( - 'sourceId', - ); - @override - late final GeneratedColumn sourceId = GeneratedColumn( - 'source_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _overrideJsonMeta = const VerificationMeta( - 'overrideJson', - ); - @override - late final GeneratedColumn overrideJson = GeneratedColumn( - 'override_json', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( - 'createdAtLocal', - ); - @override - late final GeneratedColumn createdAtLocal = GeneratedColumn( - 'created_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( - 'updatedAtLocal', - ); - @override - late final GeneratedColumn updatedAtLocal = GeneratedColumn( - 'updated_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - @override - List get $columns => [ - id, - accountId, - sourceType, - sourceId, - overrideJson, - createdAtLocal, - updatedAtLocal, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'schedule_item_overrides'; - @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); + if (data.containsKey('microsoft_reminder_date_time')) { + context.handle( + _microsoftReminderDateTimeMeta, + microsoftReminderDateTime.isAcceptableOrUnknown( + data['microsoft_reminder_date_time']!, + _microsoftReminderDateTimeMeta, + ), + ); } - if (data.containsKey('account_id')) { + if (data.containsKey('microsoft_reminder_time_zone')) { context.handle( - _accountIdMeta, - accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + _microsoftReminderTimeZoneMeta, + microsoftReminderTimeZone.isAcceptableOrUnknown( + data['microsoft_reminder_time_zone']!, + _microsoftReminderTimeZoneMeta, + ), ); - } else if (isInserting) { - context.missing(_accountIdMeta); } - if (data.containsKey('source_type')) { + if (data.containsKey('microsoft_is_reminder_on')) { context.handle( - _sourceTypeMeta, - sourceType.isAcceptableOrUnknown(data['source_type']!, _sourceTypeMeta), + _microsoftIsReminderOnMeta, + microsoftIsReminderOn.isAcceptableOrUnknown( + data['microsoft_is_reminder_on']!, + _microsoftIsReminderOnMeta, + ), ); - } else if (isInserting) { - context.missing(_sourceTypeMeta); } - if (data.containsKey('source_id')) { + if (data.containsKey('microsoft_completed_date_time')) { context.handle( - _sourceIdMeta, - sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta), + _microsoftCompletedDateTimeMeta, + microsoftCompletedDateTime.isAcceptableOrUnknown( + data['microsoft_completed_date_time']!, + _microsoftCompletedDateTimeMeta, + ), ); - } else if (isInserting) { - context.missing(_sourceIdMeta); } - if (data.containsKey('override_json')) { + if (data.containsKey('microsoft_completed_time_zone')) { context.handle( - _overrideJsonMeta, - overrideJson.isAcceptableOrUnknown( - data['override_json']!, - _overrideJsonMeta, + _microsoftCompletedTimeZoneMeta, + microsoftCompletedTimeZone.isAcceptableOrUnknown( + data['microsoft_completed_time_zone']!, + _microsoftCompletedTimeZoneMeta, ), ); - } else if (isInserting) { - context.missing(_overrideJsonMeta); } - if (data.containsKey('created_at_local')) { + if (data.containsKey('microsoft_checklist_items_json')) { context.handle( - _createdAtLocalMeta, - createdAtLocal.isAcceptableOrUnknown( - data['created_at_local']!, - _createdAtLocalMeta, + _microsoftChecklistItemsJsonMeta, + microsoftChecklistItemsJson.isAcceptableOrUnknown( + data['microsoft_checklist_items_json']!, + _microsoftChecklistItemsJsonMeta, ), ); - } else if (isInserting) { - context.missing(_createdAtLocalMeta); } - if (data.containsKey('updated_at_local')) { + if (data.containsKey('recurrence_json')) { context.handle( - _updatedAtLocalMeta, - updatedAtLocal.isAcceptableOrUnknown( - data['updated_at_local']!, - _updatedAtLocalMeta, + _recurrenceJsonMeta, + recurrenceJson.isAcceptableOrUnknown( + data['recurrence_json']!, + _recurrenceJsonMeta, ), ); - } else if (isInserting) { - context.missing(_updatedAtLocalMeta); } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - ScheduleItemOverride map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ScheduleItemOverride( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, + if (data.containsKey('importance')) { + context.handle( + _importanceMeta, + importance.isAcceptableOrUnknown(data['importance']!, _importanceMeta), + ); + } + if (data.containsKey('categories_json')) { + context.handle( + _categoriesJsonMeta, + categoriesJson.isAcceptableOrUnknown( + data['categories_json']!, + _categoriesJsonMeta, + ), + ); + } + if (data.containsKey('has_attachments')) { + context.handle( + _hasAttachmentsMeta, + hasAttachments.isAcceptableOrUnknown( + data['has_attachments']!, + _hasAttachmentsMeta, + ), + ); + } + if (data.containsKey('provider_metadata_json')) { + context.handle( + _providerMetadataJsonMeta, + providerMetadataJson.isAcceptableOrUnknown( + data['provider_metadata_json']!, + _providerMetadataJsonMeta, + ), + ); + } + if (data.containsKey('deleted')) { + context.handle( + _deletedMeta, + deleted.isAcceptableOrUnknown(data['deleted']!, _deletedMeta), + ); + } + if (data.containsKey('hidden')) { + context.handle( + _hiddenMeta, + hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta), + ); + } + if (data.containsKey('links_json')) { + context.handle( + _linksJsonMeta, + linksJson.isAcceptableOrUnknown(data['links_json']!, _linksJsonMeta), + ); + } + if (data.containsKey('web_view_link')) { + context.handle( + _webViewLinkMeta, + webViewLink.isAcceptableOrUnknown( + data['web_view_link']!, + _webViewLinkMeta, + ), + ); + } + if (data.containsKey('assignment_info_json')) { + context.handle( + _assignmentInfoJsonMeta, + assignmentInfoJson.isAcceptableOrUnknown( + data['assignment_info_json']!, + _assignmentInfoJsonMeta, + ), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } else if (isInserting) { + context.missing(_rawJsonMeta); + } + if (data.containsKey('server_missing')) { + context.handle( + _serverMissingMeta, + serverMissing.isAcceptableOrUnknown( + data['server_missing']!, + _serverMissingMeta, + ), + ); + } + if (data.containsKey('local_dirty')) { + context.handle( + _localDirtyMeta, + localDirty.isAcceptableOrUnknown(data['local_dirty']!, _localDirtyMeta), + ); + } + if (data.containsKey('pending_delete')) { + context.handle( + _pendingDeleteMeta, + pendingDelete.isAcceptableOrUnknown( + data['pending_delete']!, + _pendingDeleteMeta, + ), + ); + } + if (data.containsKey('pending_move')) { + context.handle( + _pendingMoveMeta, + pendingMove.isAcceptableOrUnknown( + data['pending_move']!, + _pendingMoveMeta, + ), + ); + } + if (data.containsKey('local_created')) { + context.handle( + _localCreatedMeta, + localCreated.isAcceptableOrUnknown( + data['local_created']!, + _localCreatedMeta, + ), + ); + } + if (data.containsKey('sync_base_updated_utc')) { + context.handle( + _syncBaseUpdatedUtcMeta, + syncBaseUpdatedUtc.isAcceptableOrUnknown( + data['sync_base_updated_utc']!, + _syncBaseUpdatedUtcMeta, + ), + ); + } + if (data.containsKey('last_synced_at_utc')) { + context.handle( + _lastSyncedAtUtcMeta, + lastSyncedAtUtc.isAcceptableOrUnknown( + data['last_synced_at_utc']!, + _lastSyncedAtUtcMeta, + ), + ); + } + if (data.containsKey('created_local_at_utc')) { + context.handle( + _createdLocalAtUtcMeta, + createdLocalAtUtc.isAcceptableOrUnknown( + data['created_local_at_utc']!, + _createdLocalAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdLocalAtUtcMeta); + } + if (data.containsKey('updated_local_at_utc')) { + context.handle( + _updatedLocalAtUtcMeta, + updatedLocalAtUtc.isAcceptableOrUnknown( + data['updated_local_at_utc']!, + _updatedLocalAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedLocalAtUtcMeta); + } + return context; + } + + @override + Set get $primaryKey => {accountId, taskListId, id}; + @override + Task map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Task( accountId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}account_id'], )!, - sourceType: attachedDatabase.typeMapping.read( + taskListId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}source_type'], + data['${effectivePrefix}task_list_id'], )!, - sourceId: attachedDatabase.typeMapping.read( + id: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}source_id'], + data['${effectivePrefix}id'], )!, - overrideJson: attachedDatabase.typeMapping.read( + davCollectionId: attachedDatabase.typeMapping.read( DriftSqlType.string, - data['${effectivePrefix}override_json'], - )!, - createdAtLocal: attachedDatabase.typeMapping.read( + data['${effectivePrefix}dav_collection_id'], + ), + davObjectId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_object_id'], + ), + davComponentId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_component_id'], + ), + icalUid: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ical_uid'], + ), + recurrenceIdKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}recurrence_id_key'], + ), + icalPriority: attachedDatabase.typeMapping.read( DriftSqlType.int, - data['${effectivePrefix}created_at_local'], - )!, - updatedAtLocal: attachedDatabase.typeMapping.read( + data['${effectivePrefix}ical_priority'], + ), + percentComplete: attachedDatabase.typeMapping.read( DriftSqlType.int, - data['${effectivePrefix}updated_at_local'], + data['${effectivePrefix}percent_complete'], + ), + taskLocation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_location'], + ), + taskUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_url'], + ), + taskClassification: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_classification'], + ), + taskPinned: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}task_pinned'], + ), + taskHideSubtasks: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}task_hide_subtasks'], + ), + taskHideCompletedSubtasks: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}task_hide_completed_subtasks'], + ), + taskAlarmsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_alarms_json'], + ), + parentUid: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}parent_uid'], + ), + sortOrder: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sort_order'], + ), + providerExtensionProjectionJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_extension_projection_json'], + ), + projectionVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}projection_version'], + )!, + kind: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}kind'], + ), + etag: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}etag'], + ), + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + )!, + updatedUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_utc'], + ), + selfLink: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}self_link'], + ), + parent: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}parent'], + ), + position: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}position'], + ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + ), + dueUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}due_utc'], + ), + completedUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}completed_utc'], + ), + providerStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_status'], + ), + bodyContent: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}body_content'], + ), + bodyContentType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}body_content_type'], + ), + microsoftDueDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_due_date_time'], + ), + microsoftDueTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_due_time_zone'], + ), + microsoftStartDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_start_date_time'], + ), + microsoftStartTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_start_time_zone'], + ), + microsoftReminderDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_reminder_date_time'], + ), + microsoftReminderTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_reminder_time_zone'], + ), + microsoftIsReminderOn: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}microsoft_is_reminder_on'], + ), + microsoftCompletedDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_completed_date_time'], + ), + microsoftCompletedTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_completed_time_zone'], + ), + microsoftChecklistItemsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}microsoft_checklist_items_json'], + ), + recurrenceJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}recurrence_json'], + ), + importance: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}importance'], + ), + categoriesJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}categories_json'], + ), + hasAttachments: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_attachments'], + ), + providerMetadataJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_metadata_json'], + ), + deleted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}deleted'], + ), + hidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}hidden'], + ), + linksJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}links_json'], + ), + webViewLink: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}web_view_link'], + ), + assignmentInfoJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}assignment_info_json'], + ), + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + )!, + serverMissing: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}server_missing'], + )!, + localDirty: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}local_dirty'], + )!, + pendingDelete: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pending_delete'], + )!, + pendingMove: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}pending_move'], + )!, + localCreated: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}local_created'], + )!, + syncBaseUpdatedUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_base_updated_utc'], + ), + lastSyncedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_synced_at_utc'], + ), + createdLocalAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_local_at_utc'], + )!, + updatedLocalAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_local_at_utc'], )!, ); } @override - $ScheduleItemOverridesTable createAlias(String alias) { - return $ScheduleItemOverridesTable(attachedDatabase, alias); - } -} - -class ScheduleItemOverride extends DataClass - implements Insertable { - final String id; - final String accountId; - final String sourceType; - final String sourceId; - final String overrideJson; - final int createdAtLocal; - final int updatedAtLocal; - const ScheduleItemOverride({ - required this.id, - required this.accountId, - required this.sourceType, - required this.sourceId, - required this.overrideJson, - required this.createdAtLocal, - required this.updatedAtLocal, - }); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['source_type'] = Variable(sourceType); - map['source_id'] = Variable(sourceId); - map['override_json'] = Variable(overrideJson); - map['created_at_local'] = Variable(createdAtLocal); - map['updated_at_local'] = Variable(updatedAtLocal); - return map; - } - - ScheduleItemOverridesCompanion toCompanion(bool nullToAbsent) { - return ScheduleItemOverridesCompanion( - id: Value(id), - accountId: Value(accountId), - sourceType: Value(sourceType), - sourceId: Value(sourceId), - overrideJson: Value(overrideJson), - createdAtLocal: Value(createdAtLocal), - updatedAtLocal: Value(updatedAtLocal), - ); - } - - factory ScheduleItemOverride.fromJson( - Map json, { - ValueSerializer? serializer, - }) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return ScheduleItemOverride( - id: serializer.fromJson(json['id']), - accountId: serializer.fromJson(json['accountId']), - sourceType: serializer.fromJson(json['sourceType']), - sourceId: serializer.fromJson(json['sourceId']), - overrideJson: serializer.fromJson(json['overrideJson']), - createdAtLocal: serializer.fromJson(json['createdAtLocal']), - updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'accountId': serializer.toJson(accountId), - 'sourceType': serializer.toJson(sourceType), - 'sourceId': serializer.toJson(sourceId), - 'overrideJson': serializer.toJson(overrideJson), - 'createdAtLocal': serializer.toJson(createdAtLocal), - 'updatedAtLocal': serializer.toJson(updatedAtLocal), - }; - } - - ScheduleItemOverride copyWith({ - String? id, - String? accountId, - String? sourceType, - String? sourceId, - String? overrideJson, - int? createdAtLocal, - int? updatedAtLocal, - }) => ScheduleItemOverride( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - sourceType: sourceType ?? this.sourceType, - sourceId: sourceId ?? this.sourceId, - overrideJson: overrideJson ?? this.overrideJson, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - ); - ScheduleItemOverride copyWithCompanion(ScheduleItemOverridesCompanion data) { - return ScheduleItemOverride( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - sourceType: data.sourceType.present - ? data.sourceType.value - : this.sourceType, - sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, - overrideJson: data.overrideJson.present - ? data.overrideJson.value - : this.overrideJson, - createdAtLocal: data.createdAtLocal.present - ? data.createdAtLocal.value - : this.createdAtLocal, - updatedAtLocal: data.updatedAtLocal.present - ? data.updatedAtLocal.value - : this.updatedAtLocal, - ); + $TasksTable createAlias(String alias) { + return $TasksTable(attachedDatabase, alias); } +} - @override - String toString() { - return (StringBuffer('ScheduleItemOverride(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('sourceType: $sourceType, ') - ..write('sourceId: $sourceId, ') - ..write('overrideJson: $overrideJson, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, - accountId, - sourceType, - sourceId, - overrideJson, - createdAtLocal, - updatedAtLocal, - ); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ScheduleItemOverride && - other.id == this.id && - other.accountId == this.accountId && - other.sourceType == this.sourceType && - other.sourceId == this.sourceId && - other.overrideJson == this.overrideJson && - other.createdAtLocal == this.createdAtLocal && - other.updatedAtLocal == this.updatedAtLocal); -} - -class ScheduleItemOverridesCompanion - extends UpdateCompanion { - final Value id; - final Value accountId; - final Value sourceType; - final Value sourceId; - final Value overrideJson; - final Value createdAtLocal; - final Value updatedAtLocal; - final Value rowid; - const ScheduleItemOverridesCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.sourceType = const Value.absent(), - this.sourceId = const Value.absent(), - this.overrideJson = const Value.absent(), - this.createdAtLocal = const Value.absent(), - this.updatedAtLocal = const Value.absent(), - this.rowid = const Value.absent(), +class Task extends DataClass implements Insertable { + final String accountId; + final String taskListId; + final String id; + final String? davCollectionId; + final String? davObjectId; + final String? davComponentId; + final String? icalUid; + final String? recurrenceIdKey; + final int? icalPriority; + final int? percentComplete; + final String? taskLocation; + final String? taskUrl; + final String? taskClassification; + final bool? taskPinned; + final bool? taskHideSubtasks; + final bool? taskHideCompletedSubtasks; + final String? taskAlarmsJson; + final String? parentUid; + final int? sortOrder; + final String? providerExtensionProjectionJson; + final int projectionVersion; + final String? kind; + final String? etag; + final String title; + final String? updatedUtc; + final String? selfLink; + final String? parent; + final String? position; + final String? notes; + final String? status; + final String? dueUtc; + final String? completedUtc; + final String? providerStatus; + final String? bodyContent; + final String? bodyContentType; + final String? microsoftDueDateTime; + final String? microsoftDueTimeZone; + final String? microsoftStartDateTime; + final String? microsoftStartTimeZone; + final String? microsoftReminderDateTime; + final String? microsoftReminderTimeZone; + final bool? microsoftIsReminderOn; + final String? microsoftCompletedDateTime; + final String? microsoftCompletedTimeZone; + final String? microsoftChecklistItemsJson; + final String? recurrenceJson; + final String? importance; + final String? categoriesJson; + final bool? hasAttachments; + final String? providerMetadataJson; + final bool? deleted; + final bool? hidden; + final String? linksJson; + final String? webViewLink; + final String? assignmentInfoJson; + final String rawJson; + final bool serverMissing; + final bool localDirty; + final bool pendingDelete; + final bool pendingMove; + final bool localCreated; + final String? syncBaseUpdatedUtc; + final String? lastSyncedAtUtc; + final String createdLocalAtUtc; + final String updatedLocalAtUtc; + const Task({ + required this.accountId, + required this.taskListId, + required this.id, + this.davCollectionId, + this.davObjectId, + this.davComponentId, + this.icalUid, + this.recurrenceIdKey, + this.icalPriority, + this.percentComplete, + this.taskLocation, + this.taskUrl, + this.taskClassification, + this.taskPinned, + this.taskHideSubtasks, + this.taskHideCompletedSubtasks, + this.taskAlarmsJson, + this.parentUid, + this.sortOrder, + this.providerExtensionProjectionJson, + required this.projectionVersion, + this.kind, + this.etag, + required this.title, + this.updatedUtc, + this.selfLink, + this.parent, + this.position, + this.notes, + this.status, + this.dueUtc, + this.completedUtc, + this.providerStatus, + this.bodyContent, + this.bodyContentType, + this.microsoftDueDateTime, + this.microsoftDueTimeZone, + this.microsoftStartDateTime, + this.microsoftStartTimeZone, + this.microsoftReminderDateTime, + this.microsoftReminderTimeZone, + this.microsoftIsReminderOn, + this.microsoftCompletedDateTime, + this.microsoftCompletedTimeZone, + this.microsoftChecklistItemsJson, + this.recurrenceJson, + this.importance, + this.categoriesJson, + this.hasAttachments, + this.providerMetadataJson, + this.deleted, + this.hidden, + this.linksJson, + this.webViewLink, + this.assignmentInfoJson, + required this.rawJson, + required this.serverMissing, + required this.localDirty, + required this.pendingDelete, + required this.pendingMove, + required this.localCreated, + this.syncBaseUpdatedUtc, + this.lastSyncedAtUtc, + required this.createdLocalAtUtc, + required this.updatedLocalAtUtc, }); - ScheduleItemOverridesCompanion.insert({ - required String id, - required String accountId, - required String sourceType, - required String sourceId, - required String overrideJson, - required int createdAtLocal, - required int updatedAtLocal, - this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - sourceType = Value(sourceType), - sourceId = Value(sourceId), - overrideJson = Value(overrideJson), - createdAtLocal = Value(createdAtLocal), - updatedAtLocal = Value(updatedAtLocal); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? sourceType, - Expression? sourceId, - Expression? overrideJson, - Expression? createdAtLocal, - Expression? updatedAtLocal, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (sourceType != null) 'source_type': sourceType, - if (sourceId != null) 'source_id': sourceId, - if (overrideJson != null) 'override_json': overrideJson, - if (createdAtLocal != null) 'created_at_local': createdAtLocal, - if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, - if (rowid != null) 'rowid': rowid, - }); - } - - ScheduleItemOverridesCompanion copyWith({ - Value? id, - Value? accountId, - Value? sourceType, - Value? sourceId, - Value? overrideJson, - Value? createdAtLocal, - Value? updatedAtLocal, - Value? rowid, - }) { - return ScheduleItemOverridesCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - sourceType: sourceType ?? this.sourceType, - sourceId: sourceId ?? this.sourceId, - overrideJson: overrideJson ?? this.overrideJson, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - rowid: rowid ?? this.rowid, - ); - } - @override Map toColumns(bool nullToAbsent) { final map = {}; - if (id.present) { - map['id'] = Variable(id.value); + map['account_id'] = Variable(accountId); + map['task_list_id'] = Variable(taskListId); + map['id'] = Variable(id); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); + if (!nullToAbsent || davObjectId != null) { + map['dav_object_id'] = Variable(davObjectId); } - if (sourceType.present) { - map['source_type'] = Variable(sourceType.value); + if (!nullToAbsent || davComponentId != null) { + map['dav_component_id'] = Variable(davComponentId); } - if (sourceId.present) { - map['source_id'] = Variable(sourceId.value); + if (!nullToAbsent || icalUid != null) { + map['ical_uid'] = Variable(icalUid); } - if (overrideJson.present) { - map['override_json'] = Variable(overrideJson.value); + if (!nullToAbsent || recurrenceIdKey != null) { + map['recurrence_id_key'] = Variable(recurrenceIdKey); } - if (createdAtLocal.present) { - map['created_at_local'] = Variable(createdAtLocal.value); + if (!nullToAbsent || icalPriority != null) { + map['ical_priority'] = Variable(icalPriority); } - if (updatedAtLocal.present) { - map['updated_at_local'] = Variable(updatedAtLocal.value); + if (!nullToAbsent || percentComplete != null) { + map['percent_complete'] = Variable(percentComplete); } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); + if (!nullToAbsent || taskLocation != null) { + map['task_location'] = Variable(taskLocation); } - return map; - } - - @override - String toString() { - return (StringBuffer('ScheduleItemOverridesCompanion(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('sourceType: $sourceType, ') - ..write('sourceId: $sourceId, ') - ..write('overrideJson: $overrideJson, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $NotificationScheduleTable extends NotificationSchedule - with TableInfo<$NotificationScheduleTable, NotificationScheduleData> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $NotificationScheduleTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _accountIdMeta = const VerificationMeta( - 'accountId', - ); - @override - late final GeneratedColumn accountId = GeneratedColumn( - 'account_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES accounts (id) ON DELETE CASCADE', - ), - ); - static const VerificationMeta _sourceTypeMeta = const VerificationMeta( - 'sourceType', - ); - @override - late final GeneratedColumn sourceType = GeneratedColumn( - 'source_type', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _sourceIdMeta = const VerificationMeta( - 'sourceId', - ); - @override - late final GeneratedColumn sourceId = GeneratedColumn( - 'source_id', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _scheduledAtUtcMeta = const VerificationMeta( - 'scheduledAtUtc', - ); - @override - late final GeneratedColumn scheduledAtUtc = GeneratedColumn( - 'scheduled_at_utc', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _titleMeta = const VerificationMeta('title'); - @override - late final GeneratedColumn title = GeneratedColumn( - 'title', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); - static const VerificationMeta _bodyMeta = const VerificationMeta('body'); - @override - late final GeneratedColumn body = GeneratedColumn( - 'body', - aliasedName, - true, - type: DriftSqlType.string, - requiredDuringInsert: false, - ); - static const VerificationMeta _sentAtUtcMeta = const VerificationMeta( - 'sentAtUtc', - ); - @override - late final GeneratedColumn sentAtUtc = GeneratedColumn( - 'sent_at_utc', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - ); - static const VerificationMeta _dismissedAtUtcMeta = const VerificationMeta( - 'dismissedAtUtc', - ); - @override - late final GeneratedColumn dismissedAtUtc = GeneratedColumn( - 'dismissed_at_utc', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - ); - static const VerificationMeta _snoozedUntilUtcMeta = const VerificationMeta( - 'snoozedUntilUtc', - ); - @override - late final GeneratedColumn snoozedUntilUtc = GeneratedColumn( - 'snoozed_until_utc', - aliasedName, - true, - type: DriftSqlType.int, - requiredDuringInsert: false, - ); - static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( - 'createdAtLocal', - ); - @override - late final GeneratedColumn createdAtLocal = GeneratedColumn( - 'created_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( - 'updatedAtLocal', - ); - @override - late final GeneratedColumn updatedAtLocal = GeneratedColumn( - 'updated_at_local', - aliasedName, - false, - type: DriftSqlType.int, - requiredDuringInsert: true, - ); - @override - List get $columns => [ - id, - accountId, - sourceType, - sourceId, - scheduledAtUtc, - title, - body, - sentAtUtc, - dismissedAtUtc, - snoozedUntilUtc, - createdAtLocal, - updatedAtLocal, - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'notification_schedule'; - @override - VerificationContext validateIntegrity( - Insertable instance, { - bool isInserting = false, - }) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); + if (!nullToAbsent || taskUrl != null) { + map['task_url'] = Variable(taskUrl); } - if (data.containsKey('account_id')) { - context.handle( - _accountIdMeta, - accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), - ); - } else if (isInserting) { - context.missing(_accountIdMeta); + if (!nullToAbsent || taskClassification != null) { + map['task_classification'] = Variable(taskClassification); } - if (data.containsKey('source_type')) { - context.handle( - _sourceTypeMeta, - sourceType.isAcceptableOrUnknown(data['source_type']!, _sourceTypeMeta), - ); - } else if (isInserting) { - context.missing(_sourceTypeMeta); + if (!nullToAbsent || taskPinned != null) { + map['task_pinned'] = Variable(taskPinned); } - if (data.containsKey('source_id')) { - context.handle( - _sourceIdMeta, - sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta), - ); - } else if (isInserting) { - context.missing(_sourceIdMeta); + if (!nullToAbsent || taskHideSubtasks != null) { + map['task_hide_subtasks'] = Variable(taskHideSubtasks); } - if (data.containsKey('scheduled_at_utc')) { - context.handle( - _scheduledAtUtcMeta, - scheduledAtUtc.isAcceptableOrUnknown( - data['scheduled_at_utc']!, - _scheduledAtUtcMeta, - ), + if (!nullToAbsent || taskHideCompletedSubtasks != null) { + map['task_hide_completed_subtasks'] = Variable( + taskHideCompletedSubtasks, ); - } else if (isInserting) { - context.missing(_scheduledAtUtcMeta); } - if (data.containsKey('title')) { - context.handle( - _titleMeta, - title.isAcceptableOrUnknown(data['title']!, _titleMeta), - ); - } else if (isInserting) { - context.missing(_titleMeta); + if (!nullToAbsent || taskAlarmsJson != null) { + map['task_alarms_json'] = Variable(taskAlarmsJson); } - if (data.containsKey('body')) { - context.handle( - _bodyMeta, - body.isAcceptableOrUnknown(data['body']!, _bodyMeta), - ); + if (!nullToAbsent || parentUid != null) { + map['parent_uid'] = Variable(parentUid); } - if (data.containsKey('sent_at_utc')) { - context.handle( - _sentAtUtcMeta, - sentAtUtc.isAcceptableOrUnknown(data['sent_at_utc']!, _sentAtUtcMeta), - ); + if (!nullToAbsent || sortOrder != null) { + map['sort_order'] = Variable(sortOrder); } - if (data.containsKey('dismissed_at_utc')) { - context.handle( - _dismissedAtUtcMeta, - dismissedAtUtc.isAcceptableOrUnknown( - data['dismissed_at_utc']!, - _dismissedAtUtcMeta, - ), + if (!nullToAbsent || providerExtensionProjectionJson != null) { + map['provider_extension_projection_json'] = Variable( + providerExtensionProjectionJson, ); } - if (data.containsKey('snoozed_until_utc')) { - context.handle( - _snoozedUntilUtcMeta, - snoozedUntilUtc.isAcceptableOrUnknown( - data['snoozed_until_utc']!, - _snoozedUntilUtcMeta, - ), - ); + map['projection_version'] = Variable(projectionVersion); + if (!nullToAbsent || kind != null) { + map['kind'] = Variable(kind); } - if (data.containsKey('created_at_local')) { - context.handle( - _createdAtLocalMeta, - createdAtLocal.isAcceptableOrUnknown( - data['created_at_local']!, - _createdAtLocalMeta, - ), - ); - } else if (isInserting) { - context.missing(_createdAtLocalMeta); + if (!nullToAbsent || etag != null) { + map['etag'] = Variable(etag); } - if (data.containsKey('updated_at_local')) { - context.handle( - _updatedAtLocalMeta, - updatedAtLocal.isAcceptableOrUnknown( - data['updated_at_local']!, - _updatedAtLocalMeta, - ), - ); - } else if (isInserting) { - context.missing(_updatedAtLocalMeta); + map['title'] = Variable(title); + if (!nullToAbsent || updatedUtc != null) { + map['updated_utc'] = Variable(updatedUtc); } - return context; - } - - @override - Set get $primaryKey => {id}; - @override - NotificationScheduleData map( - Map data, { - String? tablePrefix, - }) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return NotificationScheduleData( - id: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}id'], - )!, - accountId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}account_id'], - )!, - sourceType: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}source_type'], - )!, - sourceId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}source_id'], - )!, - scheduledAtUtc: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}scheduled_at_utc'], - )!, - title: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}title'], - )!, - body: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}body'], - ), - sentAtUtc: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}sent_at_utc'], - ), - dismissedAtUtc: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}dismissed_at_utc'], - ), - snoozedUntilUtc: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}snoozed_until_utc'], - ), - createdAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}created_at_local'], - )!, - updatedAtLocal: attachedDatabase.typeMapping.read( - DriftSqlType.int, - data['${effectivePrefix}updated_at_local'], - )!, - ); - } - - @override - $NotificationScheduleTable createAlias(String alias) { - return $NotificationScheduleTable(attachedDatabase, alias); - } -} - -class NotificationScheduleData extends DataClass - implements Insertable { - final String id; - final String accountId; - final String sourceType; - final String sourceId; - final int scheduledAtUtc; - final String title; - final String? body; - final int? sentAtUtc; - final int? dismissedAtUtc; - final int? snoozedUntilUtc; - final int createdAtLocal; - final int updatedAtLocal; - const NotificationScheduleData({ - required this.id, - required this.accountId, - required this.sourceType, - required this.sourceId, - required this.scheduledAtUtc, - required this.title, - this.body, - this.sentAtUtc, - this.dismissedAtUtc, - this.snoozedUntilUtc, - required this.createdAtLocal, - required this.updatedAtLocal, - }); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['account_id'] = Variable(accountId); - map['source_type'] = Variable(sourceType); - map['source_id'] = Variable(sourceId); - map['scheduled_at_utc'] = Variable(scheduledAtUtc); - map['title'] = Variable(title); - if (!nullToAbsent || body != null) { - map['body'] = Variable(body); + if (!nullToAbsent || selfLink != null) { + map['self_link'] = Variable(selfLink); } - if (!nullToAbsent || sentAtUtc != null) { - map['sent_at_utc'] = Variable(sentAtUtc); + if (!nullToAbsent || parent != null) { + map['parent'] = Variable(parent); } - if (!nullToAbsent || dismissedAtUtc != null) { - map['dismissed_at_utc'] = Variable(dismissedAtUtc); + if (!nullToAbsent || position != null) { + map['position'] = Variable(position); } - if (!nullToAbsent || snoozedUntilUtc != null) { - map['snoozed_until_utc'] = Variable(snoozedUntilUtc); + if (!nullToAbsent || notes != null) { + map['notes'] = Variable(notes); } - map['created_at_local'] = Variable(createdAtLocal); - map['updated_at_local'] = Variable(updatedAtLocal); + if (!nullToAbsent || status != null) { + map['status'] = Variable(status); + } + if (!nullToAbsent || dueUtc != null) { + map['due_utc'] = Variable(dueUtc); + } + if (!nullToAbsent || completedUtc != null) { + map['completed_utc'] = Variable(completedUtc); + } + if (!nullToAbsent || providerStatus != null) { + map['provider_status'] = Variable(providerStatus); + } + if (!nullToAbsent || bodyContent != null) { + map['body_content'] = Variable(bodyContent); + } + if (!nullToAbsent || bodyContentType != null) { + map['body_content_type'] = Variable(bodyContentType); + } + if (!nullToAbsent || microsoftDueDateTime != null) { + map['microsoft_due_date_time'] = Variable(microsoftDueDateTime); + } + if (!nullToAbsent || microsoftDueTimeZone != null) { + map['microsoft_due_time_zone'] = Variable(microsoftDueTimeZone); + } + if (!nullToAbsent || microsoftStartDateTime != null) { + map['microsoft_start_date_time'] = Variable( + microsoftStartDateTime, + ); + } + if (!nullToAbsent || microsoftStartTimeZone != null) { + map['microsoft_start_time_zone'] = Variable( + microsoftStartTimeZone, + ); + } + if (!nullToAbsent || microsoftReminderDateTime != null) { + map['microsoft_reminder_date_time'] = Variable( + microsoftReminderDateTime, + ); + } + if (!nullToAbsent || microsoftReminderTimeZone != null) { + map['microsoft_reminder_time_zone'] = Variable( + microsoftReminderTimeZone, + ); + } + if (!nullToAbsent || microsoftIsReminderOn != null) { + map['microsoft_is_reminder_on'] = Variable(microsoftIsReminderOn); + } + if (!nullToAbsent || microsoftCompletedDateTime != null) { + map['microsoft_completed_date_time'] = Variable( + microsoftCompletedDateTime, + ); + } + if (!nullToAbsent || microsoftCompletedTimeZone != null) { + map['microsoft_completed_time_zone'] = Variable( + microsoftCompletedTimeZone, + ); + } + if (!nullToAbsent || microsoftChecklistItemsJson != null) { + map['microsoft_checklist_items_json'] = Variable( + microsoftChecklistItemsJson, + ); + } + if (!nullToAbsent || recurrenceJson != null) { + map['recurrence_json'] = Variable(recurrenceJson); + } + if (!nullToAbsent || importance != null) { + map['importance'] = Variable(importance); + } + if (!nullToAbsent || categoriesJson != null) { + map['categories_json'] = Variable(categoriesJson); + } + if (!nullToAbsent || hasAttachments != null) { + map['has_attachments'] = Variable(hasAttachments); + } + if (!nullToAbsent || providerMetadataJson != null) { + map['provider_metadata_json'] = Variable(providerMetadataJson); + } + if (!nullToAbsent || deleted != null) { + map['deleted'] = Variable(deleted); + } + if (!nullToAbsent || hidden != null) { + map['hidden'] = Variable(hidden); + } + if (!nullToAbsent || linksJson != null) { + map['links_json'] = Variable(linksJson); + } + if (!nullToAbsent || webViewLink != null) { + map['web_view_link'] = Variable(webViewLink); + } + if (!nullToAbsent || assignmentInfoJson != null) { + map['assignment_info_json'] = Variable(assignmentInfoJson); + } + map['raw_json'] = Variable(rawJson); + map['server_missing'] = Variable(serverMissing); + map['local_dirty'] = Variable(localDirty); + map['pending_delete'] = Variable(pendingDelete); + map['pending_move'] = Variable(pendingMove); + map['local_created'] = Variable(localCreated); + if (!nullToAbsent || syncBaseUpdatedUtc != null) { + map['sync_base_updated_utc'] = Variable(syncBaseUpdatedUtc); + } + if (!nullToAbsent || lastSyncedAtUtc != null) { + map['last_synced_at_utc'] = Variable(lastSyncedAtUtc); + } + map['created_local_at_utc'] = Variable(createdLocalAtUtc); + map['updated_local_at_utc'] = Variable(updatedLocalAtUtc); return map; } - NotificationScheduleCompanion toCompanion(bool nullToAbsent) { - return NotificationScheduleCompanion( - id: Value(id), + TasksCompanion toCompanion(bool nullToAbsent) { + return TasksCompanion( accountId: Value(accountId), - sourceType: Value(sourceType), - sourceId: Value(sourceId), - scheduledAtUtc: Value(scheduledAtUtc), + taskListId: Value(taskListId), + id: Value(id), + davCollectionId: davCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionId), + davObjectId: davObjectId == null && nullToAbsent + ? const Value.absent() + : Value(davObjectId), + davComponentId: davComponentId == null && nullToAbsent + ? const Value.absent() + : Value(davComponentId), + icalUid: icalUid == null && nullToAbsent + ? const Value.absent() + : Value(icalUid), + recurrenceIdKey: recurrenceIdKey == null && nullToAbsent + ? const Value.absent() + : Value(recurrenceIdKey), + icalPriority: icalPriority == null && nullToAbsent + ? const Value.absent() + : Value(icalPriority), + percentComplete: percentComplete == null && nullToAbsent + ? const Value.absent() + : Value(percentComplete), + taskLocation: taskLocation == null && nullToAbsent + ? const Value.absent() + : Value(taskLocation), + taskUrl: taskUrl == null && nullToAbsent + ? const Value.absent() + : Value(taskUrl), + taskClassification: taskClassification == null && nullToAbsent + ? const Value.absent() + : Value(taskClassification), + taskPinned: taskPinned == null && nullToAbsent + ? const Value.absent() + : Value(taskPinned), + taskHideSubtasks: taskHideSubtasks == null && nullToAbsent + ? const Value.absent() + : Value(taskHideSubtasks), + taskHideCompletedSubtasks: + taskHideCompletedSubtasks == null && nullToAbsent + ? const Value.absent() + : Value(taskHideCompletedSubtasks), + taskAlarmsJson: taskAlarmsJson == null && nullToAbsent + ? const Value.absent() + : Value(taskAlarmsJson), + parentUid: parentUid == null && nullToAbsent + ? const Value.absent() + : Value(parentUid), + sortOrder: sortOrder == null && nullToAbsent + ? const Value.absent() + : Value(sortOrder), + providerExtensionProjectionJson: + providerExtensionProjectionJson == null && nullToAbsent + ? const Value.absent() + : Value(providerExtensionProjectionJson), + projectionVersion: Value(projectionVersion), + kind: kind == null && nullToAbsent ? const Value.absent() : Value(kind), + etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag), title: Value(title), - body: body == null && nullToAbsent ? const Value.absent() : Value(body), - sentAtUtc: sentAtUtc == null && nullToAbsent + updatedUtc: updatedUtc == null && nullToAbsent ? const Value.absent() - : Value(sentAtUtc), - dismissedAtUtc: dismissedAtUtc == null && nullToAbsent + : Value(updatedUtc), + selfLink: selfLink == null && nullToAbsent ? const Value.absent() - : Value(dismissedAtUtc), - snoozedUntilUtc: snoozedUntilUtc == null && nullToAbsent + : Value(selfLink), + parent: parent == null && nullToAbsent ? const Value.absent() - : Value(snoozedUntilUtc), - createdAtLocal: Value(createdAtLocal), - updatedAtLocal: Value(updatedAtLocal), + : Value(parent), + position: position == null && nullToAbsent + ? const Value.absent() + : Value(position), + notes: notes == null && nullToAbsent + ? const Value.absent() + : Value(notes), + status: status == null && nullToAbsent + ? const Value.absent() + : Value(status), + dueUtc: dueUtc == null && nullToAbsent + ? const Value.absent() + : Value(dueUtc), + completedUtc: completedUtc == null && nullToAbsent + ? const Value.absent() + : Value(completedUtc), + providerStatus: providerStatus == null && nullToAbsent + ? const Value.absent() + : Value(providerStatus), + bodyContent: bodyContent == null && nullToAbsent + ? const Value.absent() + : Value(bodyContent), + bodyContentType: bodyContentType == null && nullToAbsent + ? const Value.absent() + : Value(bodyContentType), + microsoftDueDateTime: microsoftDueDateTime == null && nullToAbsent + ? const Value.absent() + : Value(microsoftDueDateTime), + microsoftDueTimeZone: microsoftDueTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(microsoftDueTimeZone), + microsoftStartDateTime: microsoftStartDateTime == null && nullToAbsent + ? const Value.absent() + : Value(microsoftStartDateTime), + microsoftStartTimeZone: microsoftStartTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(microsoftStartTimeZone), + microsoftReminderDateTime: + microsoftReminderDateTime == null && nullToAbsent + ? const Value.absent() + : Value(microsoftReminderDateTime), + microsoftReminderTimeZone: + microsoftReminderTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(microsoftReminderTimeZone), + microsoftIsReminderOn: microsoftIsReminderOn == null && nullToAbsent + ? const Value.absent() + : Value(microsoftIsReminderOn), + microsoftCompletedDateTime: + microsoftCompletedDateTime == null && nullToAbsent + ? const Value.absent() + : Value(microsoftCompletedDateTime), + microsoftCompletedTimeZone: + microsoftCompletedTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(microsoftCompletedTimeZone), + microsoftChecklistItemsJson: + microsoftChecklistItemsJson == null && nullToAbsent + ? const Value.absent() + : Value(microsoftChecklistItemsJson), + recurrenceJson: recurrenceJson == null && nullToAbsent + ? const Value.absent() + : Value(recurrenceJson), + importance: importance == null && nullToAbsent + ? const Value.absent() + : Value(importance), + categoriesJson: categoriesJson == null && nullToAbsent + ? const Value.absent() + : Value(categoriesJson), + hasAttachments: hasAttachments == null && nullToAbsent + ? const Value.absent() + : Value(hasAttachments), + providerMetadataJson: providerMetadataJson == null && nullToAbsent + ? const Value.absent() + : Value(providerMetadataJson), + deleted: deleted == null && nullToAbsent + ? const Value.absent() + : Value(deleted), + hidden: hidden == null && nullToAbsent + ? const Value.absent() + : Value(hidden), + linksJson: linksJson == null && nullToAbsent + ? const Value.absent() + : Value(linksJson), + webViewLink: webViewLink == null && nullToAbsent + ? const Value.absent() + : Value(webViewLink), + assignmentInfoJson: assignmentInfoJson == null && nullToAbsent + ? const Value.absent() + : Value(assignmentInfoJson), + rawJson: Value(rawJson), + serverMissing: Value(serverMissing), + localDirty: Value(localDirty), + pendingDelete: Value(pendingDelete), + pendingMove: Value(pendingMove), + localCreated: Value(localCreated), + syncBaseUpdatedUtc: syncBaseUpdatedUtc == null && nullToAbsent + ? const Value.absent() + : Value(syncBaseUpdatedUtc), + lastSyncedAtUtc: lastSyncedAtUtc == null && nullToAbsent + ? const Value.absent() + : Value(lastSyncedAtUtc), + createdLocalAtUtc: Value(createdLocalAtUtc), + updatedLocalAtUtc: Value(updatedLocalAtUtc), ); } - factory NotificationScheduleData.fromJson( + factory Task.fromJson( Map json, { ValueSerializer? serializer, }) { serializer ??= driftRuntimeOptions.defaultSerializer; - return NotificationScheduleData( - id: serializer.fromJson(json['id']), + return Task( accountId: serializer.fromJson(json['accountId']), - sourceType: serializer.fromJson(json['sourceType']), - sourceId: serializer.fromJson(json['sourceId']), - scheduledAtUtc: serializer.fromJson(json['scheduledAtUtc']), + taskListId: serializer.fromJson(json['taskListId']), + id: serializer.fromJson(json['id']), + davCollectionId: serializer.fromJson(json['davCollectionId']), + davObjectId: serializer.fromJson(json['davObjectId']), + davComponentId: serializer.fromJson(json['davComponentId']), + icalUid: serializer.fromJson(json['icalUid']), + recurrenceIdKey: serializer.fromJson(json['recurrenceIdKey']), + icalPriority: serializer.fromJson(json['icalPriority']), + percentComplete: serializer.fromJson(json['percentComplete']), + taskLocation: serializer.fromJson(json['taskLocation']), + taskUrl: serializer.fromJson(json['taskUrl']), + taskClassification: serializer.fromJson( + json['taskClassification'], + ), + taskPinned: serializer.fromJson(json['taskPinned']), + taskHideSubtasks: serializer.fromJson(json['taskHideSubtasks']), + taskHideCompletedSubtasks: serializer.fromJson( + json['taskHideCompletedSubtasks'], + ), + taskAlarmsJson: serializer.fromJson(json['taskAlarmsJson']), + parentUid: serializer.fromJson(json['parentUid']), + sortOrder: serializer.fromJson(json['sortOrder']), + providerExtensionProjectionJson: serializer.fromJson( + json['providerExtensionProjectionJson'], + ), + projectionVersion: serializer.fromJson(json['projectionVersion']), + kind: serializer.fromJson(json['kind']), + etag: serializer.fromJson(json['etag']), title: serializer.fromJson(json['title']), - body: serializer.fromJson(json['body']), - sentAtUtc: serializer.fromJson(json['sentAtUtc']), - dismissedAtUtc: serializer.fromJson(json['dismissedAtUtc']), - snoozedUntilUtc: serializer.fromJson(json['snoozedUntilUtc']), - createdAtLocal: serializer.fromJson(json['createdAtLocal']), - updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + updatedUtc: serializer.fromJson(json['updatedUtc']), + selfLink: serializer.fromJson(json['selfLink']), + parent: serializer.fromJson(json['parent']), + position: serializer.fromJson(json['position']), + notes: serializer.fromJson(json['notes']), + status: serializer.fromJson(json['status']), + dueUtc: serializer.fromJson(json['dueUtc']), + completedUtc: serializer.fromJson(json['completedUtc']), + providerStatus: serializer.fromJson(json['providerStatus']), + bodyContent: serializer.fromJson(json['bodyContent']), + bodyContentType: serializer.fromJson(json['bodyContentType']), + microsoftDueDateTime: serializer.fromJson( + json['microsoftDueDateTime'], + ), + microsoftDueTimeZone: serializer.fromJson( + json['microsoftDueTimeZone'], + ), + microsoftStartDateTime: serializer.fromJson( + json['microsoftStartDateTime'], + ), + microsoftStartTimeZone: serializer.fromJson( + json['microsoftStartTimeZone'], + ), + microsoftReminderDateTime: serializer.fromJson( + json['microsoftReminderDateTime'], + ), + microsoftReminderTimeZone: serializer.fromJson( + json['microsoftReminderTimeZone'], + ), + microsoftIsReminderOn: serializer.fromJson( + json['microsoftIsReminderOn'], + ), + microsoftCompletedDateTime: serializer.fromJson( + json['microsoftCompletedDateTime'], + ), + microsoftCompletedTimeZone: serializer.fromJson( + json['microsoftCompletedTimeZone'], + ), + microsoftChecklistItemsJson: serializer.fromJson( + json['microsoftChecklistItemsJson'], + ), + recurrenceJson: serializer.fromJson(json['recurrenceJson']), + importance: serializer.fromJson(json['importance']), + categoriesJson: serializer.fromJson(json['categoriesJson']), + hasAttachments: serializer.fromJson(json['hasAttachments']), + providerMetadataJson: serializer.fromJson( + json['providerMetadataJson'], + ), + deleted: serializer.fromJson(json['deleted']), + hidden: serializer.fromJson(json['hidden']), + linksJson: serializer.fromJson(json['linksJson']), + webViewLink: serializer.fromJson(json['webViewLink']), + assignmentInfoJson: serializer.fromJson( + json['assignmentInfoJson'], + ), + rawJson: serializer.fromJson(json['rawJson']), + serverMissing: serializer.fromJson(json['serverMissing']), + localDirty: serializer.fromJson(json['localDirty']), + pendingDelete: serializer.fromJson(json['pendingDelete']), + pendingMove: serializer.fromJson(json['pendingMove']), + localCreated: serializer.fromJson(json['localCreated']), + syncBaseUpdatedUtc: serializer.fromJson( + json['syncBaseUpdatedUtc'], + ), + lastSyncedAtUtc: serializer.fromJson(json['lastSyncedAtUtc']), + createdLocalAtUtc: serializer.fromJson(json['createdLocalAtUtc']), + updatedLocalAtUtc: serializer.fromJson(json['updatedLocalAtUtc']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), 'accountId': serializer.toJson(accountId), - 'sourceType': serializer.toJson(sourceType), - 'sourceId': serializer.toJson(sourceId), - 'scheduledAtUtc': serializer.toJson(scheduledAtUtc), + 'taskListId': serializer.toJson(taskListId), + 'id': serializer.toJson(id), + 'davCollectionId': serializer.toJson(davCollectionId), + 'davObjectId': serializer.toJson(davObjectId), + 'davComponentId': serializer.toJson(davComponentId), + 'icalUid': serializer.toJson(icalUid), + 'recurrenceIdKey': serializer.toJson(recurrenceIdKey), + 'icalPriority': serializer.toJson(icalPriority), + 'percentComplete': serializer.toJson(percentComplete), + 'taskLocation': serializer.toJson(taskLocation), + 'taskUrl': serializer.toJson(taskUrl), + 'taskClassification': serializer.toJson(taskClassification), + 'taskPinned': serializer.toJson(taskPinned), + 'taskHideSubtasks': serializer.toJson(taskHideSubtasks), + 'taskHideCompletedSubtasks': serializer.toJson( + taskHideCompletedSubtasks, + ), + 'taskAlarmsJson': serializer.toJson(taskAlarmsJson), + 'parentUid': serializer.toJson(parentUid), + 'sortOrder': serializer.toJson(sortOrder), + 'providerExtensionProjectionJson': serializer.toJson( + providerExtensionProjectionJson, + ), + 'projectionVersion': serializer.toJson(projectionVersion), + 'kind': serializer.toJson(kind), + 'etag': serializer.toJson(etag), 'title': serializer.toJson(title), - 'body': serializer.toJson(body), - 'sentAtUtc': serializer.toJson(sentAtUtc), - 'dismissedAtUtc': serializer.toJson(dismissedAtUtc), - 'snoozedUntilUtc': serializer.toJson(snoozedUntilUtc), - 'createdAtLocal': serializer.toJson(createdAtLocal), - 'updatedAtLocal': serializer.toJson(updatedAtLocal), - }; - } - - NotificationScheduleData copyWith({ - String? id, - String? accountId, - String? sourceType, - String? sourceId, - int? scheduledAtUtc, - String? title, - Value body = const Value.absent(), - Value sentAtUtc = const Value.absent(), - Value dismissedAtUtc = const Value.absent(), - Value snoozedUntilUtc = const Value.absent(), - int? createdAtLocal, - int? updatedAtLocal, - }) => NotificationScheduleData( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - sourceType: sourceType ?? this.sourceType, - sourceId: sourceId ?? this.sourceId, - scheduledAtUtc: scheduledAtUtc ?? this.scheduledAtUtc, - title: title ?? this.title, - body: body.present ? body.value : this.body, - sentAtUtc: sentAtUtc.present ? sentAtUtc.value : this.sentAtUtc, - dismissedAtUtc: dismissedAtUtc.present - ? dismissedAtUtc.value - : this.dismissedAtUtc, - snoozedUntilUtc: snoozedUntilUtc.present - ? snoozedUntilUtc.value - : this.snoozedUntilUtc, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - ); - NotificationScheduleData copyWithCompanion( - NotificationScheduleCompanion data, - ) { - return NotificationScheduleData( - id: data.id.present ? data.id.value : this.id, - accountId: data.accountId.present ? data.accountId.value : this.accountId, - sourceType: data.sourceType.present - ? data.sourceType.value - : this.sourceType, - sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, - scheduledAtUtc: data.scheduledAtUtc.present - ? data.scheduledAtUtc.value - : this.scheduledAtUtc, - title: data.title.present ? data.title.value : this.title, - body: data.body.present ? data.body.value : this.body, - sentAtUtc: data.sentAtUtc.present ? data.sentAtUtc.value : this.sentAtUtc, - dismissedAtUtc: data.dismissedAtUtc.present - ? data.dismissedAtUtc.value - : this.dismissedAtUtc, - snoozedUntilUtc: data.snoozedUntilUtc.present - ? data.snoozedUntilUtc.value - : this.snoozedUntilUtc, - createdAtLocal: data.createdAtLocal.present - ? data.createdAtLocal.value - : this.createdAtLocal, - updatedAtLocal: data.updatedAtLocal.present - ? data.updatedAtLocal.value - : this.updatedAtLocal, - ); - } - - @override - String toString() { - return (StringBuffer('NotificationScheduleData(') - ..write('id: $id, ') - ..write('accountId: $accountId, ') - ..write('sourceType: $sourceType, ') - ..write('sourceId: $sourceId, ') - ..write('scheduledAtUtc: $scheduledAtUtc, ') - ..write('title: $title, ') - ..write('body: $body, ') - ..write('sentAtUtc: $sentAtUtc, ') - ..write('dismissedAtUtc: $dismissedAtUtc, ') - ..write('snoozedUntilUtc: $snoozedUntilUtc, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal') - ..write(')')) - .toString(); + 'updatedUtc': serializer.toJson(updatedUtc), + 'selfLink': serializer.toJson(selfLink), + 'parent': serializer.toJson(parent), + 'position': serializer.toJson(position), + 'notes': serializer.toJson(notes), + 'status': serializer.toJson(status), + 'dueUtc': serializer.toJson(dueUtc), + 'completedUtc': serializer.toJson(completedUtc), + 'providerStatus': serializer.toJson(providerStatus), + 'bodyContent': serializer.toJson(bodyContent), + 'bodyContentType': serializer.toJson(bodyContentType), + 'microsoftDueDateTime': serializer.toJson(microsoftDueDateTime), + 'microsoftDueTimeZone': serializer.toJson(microsoftDueTimeZone), + 'microsoftStartDateTime': serializer.toJson( + microsoftStartDateTime, + ), + 'microsoftStartTimeZone': serializer.toJson( + microsoftStartTimeZone, + ), + 'microsoftReminderDateTime': serializer.toJson( + microsoftReminderDateTime, + ), + 'microsoftReminderTimeZone': serializer.toJson( + microsoftReminderTimeZone, + ), + 'microsoftIsReminderOn': serializer.toJson(microsoftIsReminderOn), + 'microsoftCompletedDateTime': serializer.toJson( + microsoftCompletedDateTime, + ), + 'microsoftCompletedTimeZone': serializer.toJson( + microsoftCompletedTimeZone, + ), + 'microsoftChecklistItemsJson': serializer.toJson( + microsoftChecklistItemsJson, + ), + 'recurrenceJson': serializer.toJson(recurrenceJson), + 'importance': serializer.toJson(importance), + 'categoriesJson': serializer.toJson(categoriesJson), + 'hasAttachments': serializer.toJson(hasAttachments), + 'providerMetadataJson': serializer.toJson(providerMetadataJson), + 'deleted': serializer.toJson(deleted), + 'hidden': serializer.toJson(hidden), + 'linksJson': serializer.toJson(linksJson), + 'webViewLink': serializer.toJson(webViewLink), + 'assignmentInfoJson': serializer.toJson(assignmentInfoJson), + 'rawJson': serializer.toJson(rawJson), + 'serverMissing': serializer.toJson(serverMissing), + 'localDirty': serializer.toJson(localDirty), + 'pendingDelete': serializer.toJson(pendingDelete), + 'pendingMove': serializer.toJson(pendingMove), + 'localCreated': serializer.toJson(localCreated), + 'syncBaseUpdatedUtc': serializer.toJson(syncBaseUpdatedUtc), + 'lastSyncedAtUtc': serializer.toJson(lastSyncedAtUtc), + 'createdLocalAtUtc': serializer.toJson(createdLocalAtUtc), + 'updatedLocalAtUtc': serializer.toJson(updatedLocalAtUtc), + }; } - @override - int get hashCode => Object.hash( - id, - accountId, - sourceType, - sourceId, - scheduledAtUtc, - title, - body, - sentAtUtc, - dismissedAtUtc, - snoozedUntilUtc, - createdAtLocal, - updatedAtLocal, + Task copyWith({ + String? accountId, + String? taskListId, + String? id, + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value icalPriority = const Value.absent(), + Value percentComplete = const Value.absent(), + Value taskLocation = const Value.absent(), + Value taskUrl = const Value.absent(), + Value taskClassification = const Value.absent(), + Value taskPinned = const Value.absent(), + Value taskHideSubtasks = const Value.absent(), + Value taskHideCompletedSubtasks = const Value.absent(), + Value taskAlarmsJson = const Value.absent(), + Value parentUid = const Value.absent(), + Value sortOrder = const Value.absent(), + Value providerExtensionProjectionJson = const Value.absent(), + int? projectionVersion, + Value kind = const Value.absent(), + Value etag = const Value.absent(), + String? title, + Value updatedUtc = const Value.absent(), + Value selfLink = const Value.absent(), + Value parent = const Value.absent(), + Value position = const Value.absent(), + Value notes = const Value.absent(), + Value status = const Value.absent(), + Value dueUtc = const Value.absent(), + Value completedUtc = const Value.absent(), + Value providerStatus = const Value.absent(), + Value bodyContent = const Value.absent(), + Value bodyContentType = const Value.absent(), + Value microsoftDueDateTime = const Value.absent(), + Value microsoftDueTimeZone = const Value.absent(), + Value microsoftStartDateTime = const Value.absent(), + Value microsoftStartTimeZone = const Value.absent(), + Value microsoftReminderDateTime = const Value.absent(), + Value microsoftReminderTimeZone = const Value.absent(), + Value microsoftIsReminderOn = const Value.absent(), + Value microsoftCompletedDateTime = const Value.absent(), + Value microsoftCompletedTimeZone = const Value.absent(), + Value microsoftChecklistItemsJson = const Value.absent(), + Value recurrenceJson = const Value.absent(), + Value importance = const Value.absent(), + Value categoriesJson = const Value.absent(), + Value hasAttachments = const Value.absent(), + Value providerMetadataJson = const Value.absent(), + Value deleted = const Value.absent(), + Value hidden = const Value.absent(), + Value linksJson = const Value.absent(), + Value webViewLink = const Value.absent(), + Value assignmentInfoJson = const Value.absent(), + String? rawJson, + bool? serverMissing, + bool? localDirty, + bool? pendingDelete, + bool? pendingMove, + bool? localCreated, + Value syncBaseUpdatedUtc = const Value.absent(), + Value lastSyncedAtUtc = const Value.absent(), + String? createdLocalAtUtc, + String? updatedLocalAtUtc, + }) => Task( + accountId: accountId ?? this.accountId, + taskListId: taskListId ?? this.taskListId, + id: id ?? this.id, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + davObjectId: davObjectId.present ? davObjectId.value : this.davObjectId, + davComponentId: davComponentId.present + ? davComponentId.value + : this.davComponentId, + icalUid: icalUid.present ? icalUid.value : this.icalUid, + recurrenceIdKey: recurrenceIdKey.present + ? recurrenceIdKey.value + : this.recurrenceIdKey, + icalPriority: icalPriority.present ? icalPriority.value : this.icalPriority, + percentComplete: percentComplete.present + ? percentComplete.value + : this.percentComplete, + taskLocation: taskLocation.present ? taskLocation.value : this.taskLocation, + taskUrl: taskUrl.present ? taskUrl.value : this.taskUrl, + taskClassification: taskClassification.present + ? taskClassification.value + : this.taskClassification, + taskPinned: taskPinned.present ? taskPinned.value : this.taskPinned, + taskHideSubtasks: taskHideSubtasks.present + ? taskHideSubtasks.value + : this.taskHideSubtasks, + taskHideCompletedSubtasks: taskHideCompletedSubtasks.present + ? taskHideCompletedSubtasks.value + : this.taskHideCompletedSubtasks, + taskAlarmsJson: taskAlarmsJson.present + ? taskAlarmsJson.value + : this.taskAlarmsJson, + parentUid: parentUid.present ? parentUid.value : this.parentUid, + sortOrder: sortOrder.present ? sortOrder.value : this.sortOrder, + providerExtensionProjectionJson: providerExtensionProjectionJson.present + ? providerExtensionProjectionJson.value + : this.providerExtensionProjectionJson, + projectionVersion: projectionVersion ?? this.projectionVersion, + kind: kind.present ? kind.value : this.kind, + etag: etag.present ? etag.value : this.etag, + title: title ?? this.title, + updatedUtc: updatedUtc.present ? updatedUtc.value : this.updatedUtc, + selfLink: selfLink.present ? selfLink.value : this.selfLink, + parent: parent.present ? parent.value : this.parent, + position: position.present ? position.value : this.position, + notes: notes.present ? notes.value : this.notes, + status: status.present ? status.value : this.status, + dueUtc: dueUtc.present ? dueUtc.value : this.dueUtc, + completedUtc: completedUtc.present ? completedUtc.value : this.completedUtc, + providerStatus: providerStatus.present + ? providerStatus.value + : this.providerStatus, + bodyContent: bodyContent.present ? bodyContent.value : this.bodyContent, + bodyContentType: bodyContentType.present + ? bodyContentType.value + : this.bodyContentType, + microsoftDueDateTime: microsoftDueDateTime.present + ? microsoftDueDateTime.value + : this.microsoftDueDateTime, + microsoftDueTimeZone: microsoftDueTimeZone.present + ? microsoftDueTimeZone.value + : this.microsoftDueTimeZone, + microsoftStartDateTime: microsoftStartDateTime.present + ? microsoftStartDateTime.value + : this.microsoftStartDateTime, + microsoftStartTimeZone: microsoftStartTimeZone.present + ? microsoftStartTimeZone.value + : this.microsoftStartTimeZone, + microsoftReminderDateTime: microsoftReminderDateTime.present + ? microsoftReminderDateTime.value + : this.microsoftReminderDateTime, + microsoftReminderTimeZone: microsoftReminderTimeZone.present + ? microsoftReminderTimeZone.value + : this.microsoftReminderTimeZone, + microsoftIsReminderOn: microsoftIsReminderOn.present + ? microsoftIsReminderOn.value + : this.microsoftIsReminderOn, + microsoftCompletedDateTime: microsoftCompletedDateTime.present + ? microsoftCompletedDateTime.value + : this.microsoftCompletedDateTime, + microsoftCompletedTimeZone: microsoftCompletedTimeZone.present + ? microsoftCompletedTimeZone.value + : this.microsoftCompletedTimeZone, + microsoftChecklistItemsJson: microsoftChecklistItemsJson.present + ? microsoftChecklistItemsJson.value + : this.microsoftChecklistItemsJson, + recurrenceJson: recurrenceJson.present + ? recurrenceJson.value + : this.recurrenceJson, + importance: importance.present ? importance.value : this.importance, + categoriesJson: categoriesJson.present + ? categoriesJson.value + : this.categoriesJson, + hasAttachments: hasAttachments.present + ? hasAttachments.value + : this.hasAttachments, + providerMetadataJson: providerMetadataJson.present + ? providerMetadataJson.value + : this.providerMetadataJson, + deleted: deleted.present ? deleted.value : this.deleted, + hidden: hidden.present ? hidden.value : this.hidden, + linksJson: linksJson.present ? linksJson.value : this.linksJson, + webViewLink: webViewLink.present ? webViewLink.value : this.webViewLink, + assignmentInfoJson: assignmentInfoJson.present + ? assignmentInfoJson.value + : this.assignmentInfoJson, + rawJson: rawJson ?? this.rawJson, + serverMissing: serverMissing ?? this.serverMissing, + localDirty: localDirty ?? this.localDirty, + pendingDelete: pendingDelete ?? this.pendingDelete, + pendingMove: pendingMove ?? this.pendingMove, + localCreated: localCreated ?? this.localCreated, + syncBaseUpdatedUtc: syncBaseUpdatedUtc.present + ? syncBaseUpdatedUtc.value + : this.syncBaseUpdatedUtc, + lastSyncedAtUtc: lastSyncedAtUtc.present + ? lastSyncedAtUtc.value + : this.lastSyncedAtUtc, + createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, + updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, ); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is NotificationScheduleData && - other.id == this.id && - other.accountId == this.accountId && - other.sourceType == this.sourceType && - other.sourceId == this.sourceId && - other.scheduledAtUtc == this.scheduledAtUtc && - other.title == this.title && - other.body == this.body && - other.sentAtUtc == this.sentAtUtc && - other.dismissedAtUtc == this.dismissedAtUtc && - other.snoozedUntilUtc == this.snoozedUntilUtc && - other.createdAtLocal == this.createdAtLocal && - other.updatedAtLocal == this.updatedAtLocal); -} - -class NotificationScheduleCompanion - extends UpdateCompanion { - final Value id; - final Value accountId; - final Value sourceType; - final Value sourceId; - final Value scheduledAtUtc; - final Value title; - final Value body; - final Value sentAtUtc; - final Value dismissedAtUtc; - final Value snoozedUntilUtc; - final Value createdAtLocal; - final Value updatedAtLocal; - final Value rowid; - const NotificationScheduleCompanion({ - this.id = const Value.absent(), - this.accountId = const Value.absent(), - this.sourceType = const Value.absent(), - this.sourceId = const Value.absent(), - this.scheduledAtUtc = const Value.absent(), - this.title = const Value.absent(), - this.body = const Value.absent(), - this.sentAtUtc = const Value.absent(), - this.dismissedAtUtc = const Value.absent(), - this.snoozedUntilUtc = const Value.absent(), - this.createdAtLocal = const Value.absent(), - this.updatedAtLocal = const Value.absent(), - this.rowid = const Value.absent(), - }); - NotificationScheduleCompanion.insert({ - required String id, - required String accountId, - required String sourceType, - required String sourceId, - required int scheduledAtUtc, - required String title, - this.body = const Value.absent(), - this.sentAtUtc = const Value.absent(), - this.dismissedAtUtc = const Value.absent(), - this.snoozedUntilUtc = const Value.absent(), - required int createdAtLocal, - required int updatedAtLocal, - this.rowid = const Value.absent(), - }) : id = Value(id), - accountId = Value(accountId), - sourceType = Value(sourceType), - sourceId = Value(sourceId), - scheduledAtUtc = Value(scheduledAtUtc), - title = Value(title), - createdAtLocal = Value(createdAtLocal), - updatedAtLocal = Value(updatedAtLocal); - static Insertable custom({ - Expression? id, - Expression? accountId, - Expression? sourceType, - Expression? sourceId, - Expression? scheduledAtUtc, - Expression? title, - Expression? body, - Expression? sentAtUtc, - Expression? dismissedAtUtc, - Expression? snoozedUntilUtc, - Expression? createdAtLocal, - Expression? updatedAtLocal, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (accountId != null) 'account_id': accountId, - if (sourceType != null) 'source_type': sourceType, - if (sourceId != null) 'source_id': sourceId, - if (scheduledAtUtc != null) 'scheduled_at_utc': scheduledAtUtc, - if (title != null) 'title': title, - if (body != null) 'body': body, - if (sentAtUtc != null) 'sent_at_utc': sentAtUtc, - if (dismissedAtUtc != null) 'dismissed_at_utc': dismissedAtUtc, - if (snoozedUntilUtc != null) 'snoozed_until_utc': snoozedUntilUtc, - if (createdAtLocal != null) 'created_at_local': createdAtLocal, - if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, - if (rowid != null) 'rowid': rowid, - }); - } - - NotificationScheduleCompanion copyWith({ - Value? id, - Value? accountId, - Value? sourceType, - Value? sourceId, - Value? scheduledAtUtc, - Value? title, - Value? body, - Value? sentAtUtc, - Value? dismissedAtUtc, - Value? snoozedUntilUtc, - Value? createdAtLocal, - Value? updatedAtLocal, - Value? rowid, - }) { - return NotificationScheduleCompanion( - id: id ?? this.id, - accountId: accountId ?? this.accountId, - sourceType: sourceType ?? this.sourceType, - sourceId: sourceId ?? this.sourceId, - scheduledAtUtc: scheduledAtUtc ?? this.scheduledAtUtc, - title: title ?? this.title, - body: body ?? this.body, - sentAtUtc: sentAtUtc ?? this.sentAtUtc, - dismissedAtUtc: dismissedAtUtc ?? this.dismissedAtUtc, - snoozedUntilUtc: snoozedUntilUtc ?? this.snoozedUntilUtc, - createdAtLocal: createdAtLocal ?? this.createdAtLocal, - updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, - rowid: rowid ?? this.rowid, + Task copyWithCompanion(TasksCompanion data) { + return Task( + accountId: data.accountId.present ? data.accountId.value : this.accountId, + taskListId: data.taskListId.present + ? data.taskListId.value + : this.taskListId, + id: data.id.present ? data.id.value : this.id, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + davObjectId: data.davObjectId.present + ? data.davObjectId.value + : this.davObjectId, + davComponentId: data.davComponentId.present + ? data.davComponentId.value + : this.davComponentId, + icalUid: data.icalUid.present ? data.icalUid.value : this.icalUid, + recurrenceIdKey: data.recurrenceIdKey.present + ? data.recurrenceIdKey.value + : this.recurrenceIdKey, + icalPriority: data.icalPriority.present + ? data.icalPriority.value + : this.icalPriority, + percentComplete: data.percentComplete.present + ? data.percentComplete.value + : this.percentComplete, + taskLocation: data.taskLocation.present + ? data.taskLocation.value + : this.taskLocation, + taskUrl: data.taskUrl.present ? data.taskUrl.value : this.taskUrl, + taskClassification: data.taskClassification.present + ? data.taskClassification.value + : this.taskClassification, + taskPinned: data.taskPinned.present + ? data.taskPinned.value + : this.taskPinned, + taskHideSubtasks: data.taskHideSubtasks.present + ? data.taskHideSubtasks.value + : this.taskHideSubtasks, + taskHideCompletedSubtasks: data.taskHideCompletedSubtasks.present + ? data.taskHideCompletedSubtasks.value + : this.taskHideCompletedSubtasks, + taskAlarmsJson: data.taskAlarmsJson.present + ? data.taskAlarmsJson.value + : this.taskAlarmsJson, + parentUid: data.parentUid.present ? data.parentUid.value : this.parentUid, + sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder, + providerExtensionProjectionJson: + data.providerExtensionProjectionJson.present + ? data.providerExtensionProjectionJson.value + : this.providerExtensionProjectionJson, + projectionVersion: data.projectionVersion.present + ? data.projectionVersion.value + : this.projectionVersion, + kind: data.kind.present ? data.kind.value : this.kind, + etag: data.etag.present ? data.etag.value : this.etag, + title: data.title.present ? data.title.value : this.title, + updatedUtc: data.updatedUtc.present + ? data.updatedUtc.value + : this.updatedUtc, + selfLink: data.selfLink.present ? data.selfLink.value : this.selfLink, + parent: data.parent.present ? data.parent.value : this.parent, + position: data.position.present ? data.position.value : this.position, + notes: data.notes.present ? data.notes.value : this.notes, + status: data.status.present ? data.status.value : this.status, + dueUtc: data.dueUtc.present ? data.dueUtc.value : this.dueUtc, + completedUtc: data.completedUtc.present + ? data.completedUtc.value + : this.completedUtc, + providerStatus: data.providerStatus.present + ? data.providerStatus.value + : this.providerStatus, + bodyContent: data.bodyContent.present + ? data.bodyContent.value + : this.bodyContent, + bodyContentType: data.bodyContentType.present + ? data.bodyContentType.value + : this.bodyContentType, + microsoftDueDateTime: data.microsoftDueDateTime.present + ? data.microsoftDueDateTime.value + : this.microsoftDueDateTime, + microsoftDueTimeZone: data.microsoftDueTimeZone.present + ? data.microsoftDueTimeZone.value + : this.microsoftDueTimeZone, + microsoftStartDateTime: data.microsoftStartDateTime.present + ? data.microsoftStartDateTime.value + : this.microsoftStartDateTime, + microsoftStartTimeZone: data.microsoftStartTimeZone.present + ? data.microsoftStartTimeZone.value + : this.microsoftStartTimeZone, + microsoftReminderDateTime: data.microsoftReminderDateTime.present + ? data.microsoftReminderDateTime.value + : this.microsoftReminderDateTime, + microsoftReminderTimeZone: data.microsoftReminderTimeZone.present + ? data.microsoftReminderTimeZone.value + : this.microsoftReminderTimeZone, + microsoftIsReminderOn: data.microsoftIsReminderOn.present + ? data.microsoftIsReminderOn.value + : this.microsoftIsReminderOn, + microsoftCompletedDateTime: data.microsoftCompletedDateTime.present + ? data.microsoftCompletedDateTime.value + : this.microsoftCompletedDateTime, + microsoftCompletedTimeZone: data.microsoftCompletedTimeZone.present + ? data.microsoftCompletedTimeZone.value + : this.microsoftCompletedTimeZone, + microsoftChecklistItemsJson: data.microsoftChecklistItemsJson.present + ? data.microsoftChecklistItemsJson.value + : this.microsoftChecklistItemsJson, + recurrenceJson: data.recurrenceJson.present + ? data.recurrenceJson.value + : this.recurrenceJson, + importance: data.importance.present + ? data.importance.value + : this.importance, + categoriesJson: data.categoriesJson.present + ? data.categoriesJson.value + : this.categoriesJson, + hasAttachments: data.hasAttachments.present + ? data.hasAttachments.value + : this.hasAttachments, + providerMetadataJson: data.providerMetadataJson.present + ? data.providerMetadataJson.value + : this.providerMetadataJson, + deleted: data.deleted.present ? data.deleted.value : this.deleted, + hidden: data.hidden.present ? data.hidden.value : this.hidden, + linksJson: data.linksJson.present ? data.linksJson.value : this.linksJson, + webViewLink: data.webViewLink.present + ? data.webViewLink.value + : this.webViewLink, + assignmentInfoJson: data.assignmentInfoJson.present + ? data.assignmentInfoJson.value + : this.assignmentInfoJson, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + serverMissing: data.serverMissing.present + ? data.serverMissing.value + : this.serverMissing, + localDirty: data.localDirty.present + ? data.localDirty.value + : this.localDirty, + pendingDelete: data.pendingDelete.present + ? data.pendingDelete.value + : this.pendingDelete, + pendingMove: data.pendingMove.present + ? data.pendingMove.value + : this.pendingMove, + localCreated: data.localCreated.present + ? data.localCreated.value + : this.localCreated, + syncBaseUpdatedUtc: data.syncBaseUpdatedUtc.present + ? data.syncBaseUpdatedUtc.value + : this.syncBaseUpdatedUtc, + lastSyncedAtUtc: data.lastSyncedAtUtc.present + ? data.lastSyncedAtUtc.value + : this.lastSyncedAtUtc, + createdLocalAtUtc: data.createdLocalAtUtc.present + ? data.createdLocalAtUtc.value + : this.createdLocalAtUtc, + updatedLocalAtUtc: data.updatedLocalAtUtc.present + ? data.updatedLocalAtUtc.value + : this.updatedLocalAtUtc, ); } - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (accountId.present) { - map['account_id'] = Variable(accountId.value); - } - if (sourceType.present) { - map['source_type'] = Variable(sourceType.value); - } - if (sourceId.present) { - map['source_id'] = Variable(sourceId.value); - } - if (scheduledAtUtc.present) { - map['scheduled_at_utc'] = Variable(scheduledAtUtc.value); - } - if (title.present) { - map['title'] = Variable(title.value); - } - if (body.present) { - map['body'] = Variable(body.value); - } - if (sentAtUtc.present) { - map['sent_at_utc'] = Variable(sentAtUtc.value); - } - if (dismissedAtUtc.present) { - map['dismissed_at_utc'] = Variable(dismissedAtUtc.value); - } - if (snoozedUntilUtc.present) { - map['snoozed_until_utc'] = Variable(snoozedUntilUtc.value); - } - if (createdAtLocal.present) { - map['created_at_local'] = Variable(createdAtLocal.value); - } - if (updatedAtLocal.present) { - map['updated_at_local'] = Variable(updatedAtLocal.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; - } - @override String toString() { - return (StringBuffer('NotificationScheduleCompanion(') - ..write('id: $id, ') + return (StringBuffer('Task(') ..write('accountId: $accountId, ') - ..write('sourceType: $sourceType, ') - ..write('sourceId: $sourceId, ') - ..write('scheduledAtUtc: $scheduledAtUtc, ') + ..write('taskListId: $taskListId, ') + ..write('id: $id, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('davComponentId: $davComponentId, ') + ..write('icalUid: $icalUid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('icalPriority: $icalPriority, ') + ..write('percentComplete: $percentComplete, ') + ..write('taskLocation: $taskLocation, ') + ..write('taskUrl: $taskUrl, ') + ..write('taskClassification: $taskClassification, ') + ..write('taskPinned: $taskPinned, ') + ..write('taskHideSubtasks: $taskHideSubtasks, ') + ..write('taskHideCompletedSubtasks: $taskHideCompletedSubtasks, ') + ..write('taskAlarmsJson: $taskAlarmsJson, ') + ..write('parentUid: $parentUid, ') + ..write('sortOrder: $sortOrder, ') + ..write( + 'providerExtensionProjectionJson: $providerExtensionProjectionJson, ', + ) + ..write('projectionVersion: $projectionVersion, ') + ..write('kind: $kind, ') + ..write('etag: $etag, ') ..write('title: $title, ') - ..write('body: $body, ') - ..write('sentAtUtc: $sentAtUtc, ') - ..write('dismissedAtUtc: $dismissedAtUtc, ') - ..write('snoozedUntilUtc: $snoozedUntilUtc, ') - ..write('createdAtLocal: $createdAtLocal, ') - ..write('updatedAtLocal: $updatedAtLocal, ') - ..write('rowid: $rowid') + ..write('updatedUtc: $updatedUtc, ') + ..write('selfLink: $selfLink, ') + ..write('parent: $parent, ') + ..write('position: $position, ') + ..write('notes: $notes, ') + ..write('status: $status, ') + ..write('dueUtc: $dueUtc, ') + ..write('completedUtc: $completedUtc, ') + ..write('providerStatus: $providerStatus, ') + ..write('bodyContent: $bodyContent, ') + ..write('bodyContentType: $bodyContentType, ') + ..write('microsoftDueDateTime: $microsoftDueDateTime, ') + ..write('microsoftDueTimeZone: $microsoftDueTimeZone, ') + ..write('microsoftStartDateTime: $microsoftStartDateTime, ') + ..write('microsoftStartTimeZone: $microsoftStartTimeZone, ') + ..write('microsoftReminderDateTime: $microsoftReminderDateTime, ') + ..write('microsoftReminderTimeZone: $microsoftReminderTimeZone, ') + ..write('microsoftIsReminderOn: $microsoftIsReminderOn, ') + ..write('microsoftCompletedDateTime: $microsoftCompletedDateTime, ') + ..write('microsoftCompletedTimeZone: $microsoftCompletedTimeZone, ') + ..write('microsoftChecklistItemsJson: $microsoftChecklistItemsJson, ') + ..write('recurrenceJson: $recurrenceJson, ') + ..write('importance: $importance, ') + ..write('categoriesJson: $categoriesJson, ') + ..write('hasAttachments: $hasAttachments, ') + ..write('providerMetadataJson: $providerMetadataJson, ') + ..write('deleted: $deleted, ') + ..write('hidden: $hidden, ') + ..write('linksJson: $linksJson, ') + ..write('webViewLink: $webViewLink, ') + ..write('assignmentInfoJson: $assignmentInfoJson, ') + ..write('rawJson: $rawJson, ') + ..write('serverMissing: $serverMissing, ') + ..write('localDirty: $localDirty, ') + ..write('pendingDelete: $pendingDelete, ') + ..write('pendingMove: $pendingMove, ') + ..write('localCreated: $localCreated, ') + ..write('syncBaseUpdatedUtc: $syncBaseUpdatedUtc, ') + ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') + ..write('createdLocalAtUtc: $createdLocalAtUtc, ') + ..write('updatedLocalAtUtc: $updatedLocalAtUtc') ..write(')')) .toString(); } -} -abstract class _$AppDatabase extends GeneratedDatabase { - _$AppDatabase(QueryExecutor e) : super(e); - $AppDatabaseManager get managers => $AppDatabaseManager(this); - late final $AccountsTable accounts = $AccountsTable(this); - late final $TaskListsTable taskLists = $TaskListsTable(this); - late final $TasksTable tasks = $TasksTable(this); - late final $PendingOpsTable pendingOps = $PendingOpsTable(this); - late final $SyncRunsTable syncRuns = $SyncRunsTable(this); - late final $CalendarSourcesTable calendarSources = $CalendarSourcesTable( - this, - ); - late final $CalendarEventsTable calendarEvents = $CalendarEventsTable(this); - late final $CalendarEventAttendeesTable calendarEventAttendees = - $CalendarEventAttendeesTable(this); - late final $CalendarEventRemindersTable calendarEventReminders = - $CalendarEventRemindersTable(this); - late final $CalendarSyncStatesTable calendarSyncStates = - $CalendarSyncStatesTable(this); - late final $CalendarColorsTable calendarColors = $CalendarColorsTable(this); - late final $ScheduleItemOverridesTable scheduleItemOverrides = - $ScheduleItemOverridesTable(this); - late final $NotificationScheduleTable notificationSchedule = - $NotificationScheduleTable(this); - late final TaskListsDao taskListsDao = TaskListsDao(this as AppDatabase); - late final TasksDao tasksDao = TasksDao(this as AppDatabase); - late final PendingOpsDao pendingOpsDao = PendingOpsDao(this as AppDatabase); - late final SyncRunsDao syncRunsDao = SyncRunsDao(this as AppDatabase); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [ - accounts, - taskLists, - tasks, - pendingOps, - syncRuns, - calendarSources, - calendarEvents, - calendarEventAttendees, - calendarEventReminders, - calendarSyncStates, - calendarColors, - scheduleItemOverrides, - notificationSchedule, - ]; @override - StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('task_lists', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('tasks', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('pending_ops', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('sync_runs', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('calendar_sources', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('calendar_events', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'calendar_sources', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('calendar_events', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'calendar_events', - limitUpdateKind: UpdateKind.delete, - ), - result: [ - TableUpdate('calendar_event_attendees', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'calendar_events', - limitUpdateKind: UpdateKind.delete, - ), - result: [ - TableUpdate('calendar_event_reminders', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('calendar_sync_states', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'calendar_sources', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('calendar_sync_states', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('schedule_item_overrides', kind: UpdateKind.delete)], - ), - WritePropagation( - on: TableUpdateQuery.onTableName( - 'accounts', - limitUpdateKind: UpdateKind.delete, - ), - result: [TableUpdate('notification_schedule', kind: UpdateKind.delete)], - ), + int get hashCode => Object.hashAll([ + accountId, + taskListId, + id, + davCollectionId, + davObjectId, + davComponentId, + icalUid, + recurrenceIdKey, + icalPriority, + percentComplete, + taskLocation, + taskUrl, + taskClassification, + taskPinned, + taskHideSubtasks, + taskHideCompletedSubtasks, + taskAlarmsJson, + parentUid, + sortOrder, + providerExtensionProjectionJson, + projectionVersion, + kind, + etag, + title, + updatedUtc, + selfLink, + parent, + position, + notes, + status, + dueUtc, + completedUtc, + providerStatus, + bodyContent, + bodyContentType, + microsoftDueDateTime, + microsoftDueTimeZone, + microsoftStartDateTime, + microsoftStartTimeZone, + microsoftReminderDateTime, + microsoftReminderTimeZone, + microsoftIsReminderOn, + microsoftCompletedDateTime, + microsoftCompletedTimeZone, + microsoftChecklistItemsJson, + recurrenceJson, + importance, + categoriesJson, + hasAttachments, + providerMetadataJson, + deleted, + hidden, + linksJson, + webViewLink, + assignmentInfoJson, + rawJson, + serverMissing, + localDirty, + pendingDelete, + pendingMove, + localCreated, + syncBaseUpdatedUtc, + lastSyncedAtUtc, + createdLocalAtUtc, + updatedLocalAtUtc, ]); -} - -typedef $$AccountsTableCreateCompanionBuilder = - AccountsCompanion Function({ - required String id, - Value provider, - Value providerAccountId, - Value displayName, - Value email, - Value tenantId, - Value accountAvatarUrl, - Value providerMetadataJson, - Value authState, - Value calendarsEnabled, - Value tasksEnabled, - Value grantedScopes, - required String createdAtUtc, - required String updatedAtUtc, - Value lastSuccessfulSyncAtUtc, - Value lastFullSyncAtUtc, - Value rowid, - }); -typedef $$AccountsTableUpdateCompanionBuilder = - AccountsCompanion Function({ - Value id, - Value provider, - Value providerAccountId, - Value displayName, - Value email, - Value tenantId, - Value accountAvatarUrl, - Value providerMetadataJson, - Value authState, - Value calendarsEnabled, - Value tasksEnabled, - Value grantedScopes, - Value createdAtUtc, - Value updatedAtUtc, - Value lastSuccessfulSyncAtUtc, - Value lastFullSyncAtUtc, - Value rowid, - }); - -final class $$AccountsTableReferences - extends BaseReferences<_$AppDatabase, $AccountsTable, Account> { - $$AccountsTableReferences(super.$_db, super.$_table, super.$_typedResult); - - static MultiTypedResultKey<$TaskListsTable, List> - _taskListsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.taskLists, - aliasName: $_aliasNameGenerator(db.accounts.id, db.taskLists.accountId), - ); - - $$TaskListsTableProcessedTableManager get taskListsRefs { - final manager = $$TaskListsTableTableManager( - $_db, - $_db.taskLists, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_taskListsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$TasksTable, List> _tasksRefsTable( - _$AppDatabase db, - ) => MultiTypedResultKey.fromTable( - db.tasks, - aliasName: $_aliasNameGenerator(db.accounts.id, db.tasks.accountId), - ); - - $$TasksTableProcessedTableManager get tasksRefs { - final manager = $$TasksTableTableManager( - $_db, - $_db.tasks, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_tasksRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$PendingOpsTable, List> - _pendingOpsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.pendingOps, - aliasName: $_aliasNameGenerator(db.accounts.id, db.pendingOps.accountId), - ); - - $$PendingOpsTableProcessedTableManager get pendingOpsRefs { - final manager = $$PendingOpsTableTableManager( - $_db, - $_db.pendingOps, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_pendingOpsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$SyncRunsTable, List> _syncRunsRefsTable( - _$AppDatabase db, - ) => MultiTypedResultKey.fromTable( - db.syncRuns, - aliasName: $_aliasNameGenerator(db.accounts.id, db.syncRuns.accountId), - ); - - $$SyncRunsTableProcessedTableManager get syncRunsRefs { - final manager = $$SyncRunsTableTableManager( - $_db, - $_db.syncRuns, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_syncRunsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$CalendarSourcesTable, List> - _calendarSourcesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.calendarSources, - aliasName: $_aliasNameGenerator( - db.accounts.id, - db.calendarSources.accountId, - ), - ); - - $$CalendarSourcesTableProcessedTableManager get calendarSourcesRefs { - final manager = $$CalendarSourcesTableTableManager( - $_db, - $_db.calendarSources, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull( - _calendarSourcesRefsTable($_db), - ); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$CalendarEventsTable, List> - _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( - db.calendarEvents, - aliasName: $_aliasNameGenerator( - db.accounts.id, - db.calendarEvents.accountId, - ), - ); - - $$CalendarEventsTableProcessedTableManager get calendarEventsRefs { - final manager = $$CalendarEventsTableTableManager( - $_db, - $_db.calendarEvents, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_calendarEventsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey<$CalendarSyncStatesTable, List> - _calendarSyncStatesRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable( - db.calendarSyncStates, - aliasName: $_aliasNameGenerator( - db.accounts.id, - db.calendarSyncStates.accountId, - ), - ); - - $$CalendarSyncStatesTableProcessedTableManager get calendarSyncStatesRefs { - final manager = $$CalendarSyncStatesTableTableManager( - $_db, - $_db.calendarSyncStates, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull( - _calendarSyncStatesRefsTable($_db), - ); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } - - static MultiTypedResultKey< - $ScheduleItemOverridesTable, - List - > - _scheduleItemOverridesRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable( - db.scheduleItemOverrides, - aliasName: $_aliasNameGenerator( - db.accounts.id, - db.scheduleItemOverrides.accountId, - ), - ); - - $$ScheduleItemOverridesTableProcessedTableManager - get scheduleItemOverridesRefs { - final manager = $$ScheduleItemOverridesTableTableManager( - $_db, - $_db.scheduleItemOverrides, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull( - _scheduleItemOverridesRefsTable($_db), - ); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Task && + other.accountId == this.accountId && + other.taskListId == this.taskListId && + other.id == this.id && + other.davCollectionId == this.davCollectionId && + other.davObjectId == this.davObjectId && + other.davComponentId == this.davComponentId && + other.icalUid == this.icalUid && + other.recurrenceIdKey == this.recurrenceIdKey && + other.icalPriority == this.icalPriority && + other.percentComplete == this.percentComplete && + other.taskLocation == this.taskLocation && + other.taskUrl == this.taskUrl && + other.taskClassification == this.taskClassification && + other.taskPinned == this.taskPinned && + other.taskHideSubtasks == this.taskHideSubtasks && + other.taskHideCompletedSubtasks == this.taskHideCompletedSubtasks && + other.taskAlarmsJson == this.taskAlarmsJson && + other.parentUid == this.parentUid && + other.sortOrder == this.sortOrder && + other.providerExtensionProjectionJson == + this.providerExtensionProjectionJson && + other.projectionVersion == this.projectionVersion && + other.kind == this.kind && + other.etag == this.etag && + other.title == this.title && + other.updatedUtc == this.updatedUtc && + other.selfLink == this.selfLink && + other.parent == this.parent && + other.position == this.position && + other.notes == this.notes && + other.status == this.status && + other.dueUtc == this.dueUtc && + other.completedUtc == this.completedUtc && + other.providerStatus == this.providerStatus && + other.bodyContent == this.bodyContent && + other.bodyContentType == this.bodyContentType && + other.microsoftDueDateTime == this.microsoftDueDateTime && + other.microsoftDueTimeZone == this.microsoftDueTimeZone && + other.microsoftStartDateTime == this.microsoftStartDateTime && + other.microsoftStartTimeZone == this.microsoftStartTimeZone && + other.microsoftReminderDateTime == this.microsoftReminderDateTime && + other.microsoftReminderTimeZone == this.microsoftReminderTimeZone && + other.microsoftIsReminderOn == this.microsoftIsReminderOn && + other.microsoftCompletedDateTime == this.microsoftCompletedDateTime && + other.microsoftCompletedTimeZone == this.microsoftCompletedTimeZone && + other.microsoftChecklistItemsJson == + this.microsoftChecklistItemsJson && + other.recurrenceJson == this.recurrenceJson && + other.importance == this.importance && + other.categoriesJson == this.categoriesJson && + other.hasAttachments == this.hasAttachments && + other.providerMetadataJson == this.providerMetadataJson && + other.deleted == this.deleted && + other.hidden == this.hidden && + other.linksJson == this.linksJson && + other.webViewLink == this.webViewLink && + other.assignmentInfoJson == this.assignmentInfoJson && + other.rawJson == this.rawJson && + other.serverMissing == this.serverMissing && + other.localDirty == this.localDirty && + other.pendingDelete == this.pendingDelete && + other.pendingMove == this.pendingMove && + other.localCreated == this.localCreated && + other.syncBaseUpdatedUtc == this.syncBaseUpdatedUtc && + other.lastSyncedAtUtc == this.lastSyncedAtUtc && + other.createdLocalAtUtc == this.createdLocalAtUtc && + other.updatedLocalAtUtc == this.updatedLocalAtUtc); +} - static MultiTypedResultKey< - $NotificationScheduleTable, - List - > - _notificationScheduleRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable( - db.notificationSchedule, - aliasName: $_aliasNameGenerator( - db.accounts.id, - db.notificationSchedule.accountId, - ), - ); - - $$NotificationScheduleTableProcessedTableManager - get notificationScheduleRefs { - final manager = $$NotificationScheduleTableTableManager( - $_db, - $_db.notificationSchedule, - ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull( - _notificationScheduleRefsTable($_db), - ); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache), - ); - } -} - -class $$AccountsTableFilterComposer - extends Composer<_$AppDatabase, $AccountsTable> { - $$AccountsTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, +class TasksCompanion extends UpdateCompanion { + final Value accountId; + final Value taskListId; + final Value id; + final Value davCollectionId; + final Value davObjectId; + final Value davComponentId; + final Value icalUid; + final Value recurrenceIdKey; + final Value icalPriority; + final Value percentComplete; + final Value taskLocation; + final Value taskUrl; + final Value taskClassification; + final Value taskPinned; + final Value taskHideSubtasks; + final Value taskHideCompletedSubtasks; + final Value taskAlarmsJson; + final Value parentUid; + final Value sortOrder; + final Value providerExtensionProjectionJson; + final Value projectionVersion; + final Value kind; + final Value etag; + final Value title; + final Value updatedUtc; + final Value selfLink; + final Value parent; + final Value position; + final Value notes; + final Value status; + final Value dueUtc; + final Value completedUtc; + final Value providerStatus; + final Value bodyContent; + final Value bodyContentType; + final Value microsoftDueDateTime; + final Value microsoftDueTimeZone; + final Value microsoftStartDateTime; + final Value microsoftStartTimeZone; + final Value microsoftReminderDateTime; + final Value microsoftReminderTimeZone; + final Value microsoftIsReminderOn; + final Value microsoftCompletedDateTime; + final Value microsoftCompletedTimeZone; + final Value microsoftChecklistItemsJson; + final Value recurrenceJson; + final Value importance; + final Value categoriesJson; + final Value hasAttachments; + final Value providerMetadataJson; + final Value deleted; + final Value hidden; + final Value linksJson; + final Value webViewLink; + final Value assignmentInfoJson; + final Value rawJson; + final Value serverMissing; + final Value localDirty; + final Value pendingDelete; + final Value pendingMove; + final Value localCreated; + final Value syncBaseUpdatedUtc; + final Value lastSyncedAtUtc; + final Value createdLocalAtUtc; + final Value updatedLocalAtUtc; + final Value rowid; + const TasksCompanion({ + this.accountId = const Value.absent(), + this.taskListId = const Value.absent(), + this.id = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davComponentId = const Value.absent(), + this.icalUid = const Value.absent(), + this.recurrenceIdKey = const Value.absent(), + this.icalPriority = const Value.absent(), + this.percentComplete = const Value.absent(), + this.taskLocation = const Value.absent(), + this.taskUrl = const Value.absent(), + this.taskClassification = const Value.absent(), + this.taskPinned = const Value.absent(), + this.taskHideSubtasks = const Value.absent(), + this.taskHideCompletedSubtasks = const Value.absent(), + this.taskAlarmsJson = const Value.absent(), + this.parentUid = const Value.absent(), + this.sortOrder = const Value.absent(), + this.providerExtensionProjectionJson = const Value.absent(), + this.projectionVersion = const Value.absent(), + this.kind = const Value.absent(), + this.etag = const Value.absent(), + this.title = const Value.absent(), + this.updatedUtc = const Value.absent(), + this.selfLink = const Value.absent(), + this.parent = const Value.absent(), + this.position = const Value.absent(), + this.notes = const Value.absent(), + this.status = const Value.absent(), + this.dueUtc = const Value.absent(), + this.completedUtc = const Value.absent(), + this.providerStatus = const Value.absent(), + this.bodyContent = const Value.absent(), + this.bodyContentType = const Value.absent(), + this.microsoftDueDateTime = const Value.absent(), + this.microsoftDueTimeZone = const Value.absent(), + this.microsoftStartDateTime = const Value.absent(), + this.microsoftStartTimeZone = const Value.absent(), + this.microsoftReminderDateTime = const Value.absent(), + this.microsoftReminderTimeZone = const Value.absent(), + this.microsoftIsReminderOn = const Value.absent(), + this.microsoftCompletedDateTime = const Value.absent(), + this.microsoftCompletedTimeZone = const Value.absent(), + this.microsoftChecklistItemsJson = const Value.absent(), + this.recurrenceJson = const Value.absent(), + this.importance = const Value.absent(), + this.categoriesJson = const Value.absent(), + this.hasAttachments = const Value.absent(), + this.providerMetadataJson = const Value.absent(), + this.deleted = const Value.absent(), + this.hidden = const Value.absent(), + this.linksJson = const Value.absent(), + this.webViewLink = const Value.absent(), + this.assignmentInfoJson = const Value.absent(), + this.rawJson = const Value.absent(), + this.serverMissing = const Value.absent(), + this.localDirty = const Value.absent(), + this.pendingDelete = const Value.absent(), + this.pendingMove = const Value.absent(), + this.localCreated = const Value.absent(), + this.syncBaseUpdatedUtc = const Value.absent(), + this.lastSyncedAtUtc = const Value.absent(), + this.createdLocalAtUtc = const Value.absent(), + this.updatedLocalAtUtc = const Value.absent(), + this.rowid = const Value.absent(), }); - ColumnFilters get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get provider => $composableBuilder( - column: $table.provider, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get providerAccountId => $composableBuilder( - column: $table.providerAccountId, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get email => $composableBuilder( - column: $table.email, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get tenantId => $composableBuilder( - column: $table.tenantId, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get accountAvatarUrl => $composableBuilder( - column: $table.accountAvatarUrl, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get providerMetadataJson => $composableBuilder( - column: $table.providerMetadataJson, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get authState => $composableBuilder( - column: $table.authState, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get calendarsEnabled => $composableBuilder( - column: $table.calendarsEnabled, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get tasksEnabled => $composableBuilder( - column: $table.tasksEnabled, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get grantedScopes => $composableBuilder( - column: $table.grantedScopes, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get createdAtUtc => $composableBuilder( - column: $table.createdAtUtc, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get updatedAtUtc => $composableBuilder( - column: $table.updatedAtUtc, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get lastSuccessfulSyncAtUtc => $composableBuilder( - column: $table.lastSuccessfulSyncAtUtc, - builder: (column) => ColumnFilters(column), - ); - - ColumnFilters get lastFullSyncAtUtc => $composableBuilder( - column: $table.lastFullSyncAtUtc, - builder: (column) => ColumnFilters(column), - ); - - Expression taskListsRefs( - Expression Function($$TaskListsTableFilterComposer f) f, - ) { - final $$TaskListsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.taskLists, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$TaskListsTableFilterComposer( - $db: $db, - $table: $db.taskLists, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression tasksRefs( - Expression Function($$TasksTableFilterComposer f) f, - ) { - final $$TasksTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.tasks, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$TasksTableFilterComposer( - $db: $db, - $table: $db.tasks, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression pendingOpsRefs( - Expression Function($$PendingOpsTableFilterComposer f) f, - ) { - final $$PendingOpsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pendingOps, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PendingOpsTableFilterComposer( - $db: $db, - $table: $db.pendingOps, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression syncRunsRefs( - Expression Function($$SyncRunsTableFilterComposer f) f, - ) { - final $$SyncRunsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.syncRuns, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$SyncRunsTableFilterComposer( - $db: $db, - $table: $db.syncRuns, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression calendarSourcesRefs( - Expression Function($$CalendarSourcesTableFilterComposer f) f, - ) { - final $$CalendarSourcesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSources, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$CalendarSourcesTableFilterComposer( - $db: $db, - $table: $db.calendarSources, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); + TasksCompanion.insert({ + required String accountId, + required String taskListId, + required String id, + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davComponentId = const Value.absent(), + this.icalUid = const Value.absent(), + this.recurrenceIdKey = const Value.absent(), + this.icalPriority = const Value.absent(), + this.percentComplete = const Value.absent(), + this.taskLocation = const Value.absent(), + this.taskUrl = const Value.absent(), + this.taskClassification = const Value.absent(), + this.taskPinned = const Value.absent(), + this.taskHideSubtasks = const Value.absent(), + this.taskHideCompletedSubtasks = const Value.absent(), + this.taskAlarmsJson = const Value.absent(), + this.parentUid = const Value.absent(), + this.sortOrder = const Value.absent(), + this.providerExtensionProjectionJson = const Value.absent(), + this.projectionVersion = const Value.absent(), + this.kind = const Value.absent(), + this.etag = const Value.absent(), + required String title, + this.updatedUtc = const Value.absent(), + this.selfLink = const Value.absent(), + this.parent = const Value.absent(), + this.position = const Value.absent(), + this.notes = const Value.absent(), + this.status = const Value.absent(), + this.dueUtc = const Value.absent(), + this.completedUtc = const Value.absent(), + this.providerStatus = const Value.absent(), + this.bodyContent = const Value.absent(), + this.bodyContentType = const Value.absent(), + this.microsoftDueDateTime = const Value.absent(), + this.microsoftDueTimeZone = const Value.absent(), + this.microsoftStartDateTime = const Value.absent(), + this.microsoftStartTimeZone = const Value.absent(), + this.microsoftReminderDateTime = const Value.absent(), + this.microsoftReminderTimeZone = const Value.absent(), + this.microsoftIsReminderOn = const Value.absent(), + this.microsoftCompletedDateTime = const Value.absent(), + this.microsoftCompletedTimeZone = const Value.absent(), + this.microsoftChecklistItemsJson = const Value.absent(), + this.recurrenceJson = const Value.absent(), + this.importance = const Value.absent(), + this.categoriesJson = const Value.absent(), + this.hasAttachments = const Value.absent(), + this.providerMetadataJson = const Value.absent(), + this.deleted = const Value.absent(), + this.hidden = const Value.absent(), + this.linksJson = const Value.absent(), + this.webViewLink = const Value.absent(), + this.assignmentInfoJson = const Value.absent(), + required String rawJson, + this.serverMissing = const Value.absent(), + this.localDirty = const Value.absent(), + this.pendingDelete = const Value.absent(), + this.pendingMove = const Value.absent(), + this.localCreated = const Value.absent(), + this.syncBaseUpdatedUtc = const Value.absent(), + this.lastSyncedAtUtc = const Value.absent(), + required String createdLocalAtUtc, + required String updatedLocalAtUtc, + this.rowid = const Value.absent(), + }) : accountId = Value(accountId), + taskListId = Value(taskListId), + id = Value(id), + title = Value(title), + rawJson = Value(rawJson), + createdLocalAtUtc = Value(createdLocalAtUtc), + updatedLocalAtUtc = Value(updatedLocalAtUtc); + static Insertable custom({ + Expression? accountId, + Expression? taskListId, + Expression? id, + Expression? davCollectionId, + Expression? davObjectId, + Expression? davComponentId, + Expression? icalUid, + Expression? recurrenceIdKey, + Expression? icalPriority, + Expression? percentComplete, + Expression? taskLocation, + Expression? taskUrl, + Expression? taskClassification, + Expression? taskPinned, + Expression? taskHideSubtasks, + Expression? taskHideCompletedSubtasks, + Expression? taskAlarmsJson, + Expression? parentUid, + Expression? sortOrder, + Expression? providerExtensionProjectionJson, + Expression? projectionVersion, + Expression? kind, + Expression? etag, + Expression? title, + Expression? updatedUtc, + Expression? selfLink, + Expression? parent, + Expression? position, + Expression? notes, + Expression? status, + Expression? dueUtc, + Expression? completedUtc, + Expression? providerStatus, + Expression? bodyContent, + Expression? bodyContentType, + Expression? microsoftDueDateTime, + Expression? microsoftDueTimeZone, + Expression? microsoftStartDateTime, + Expression? microsoftStartTimeZone, + Expression? microsoftReminderDateTime, + Expression? microsoftReminderTimeZone, + Expression? microsoftIsReminderOn, + Expression? microsoftCompletedDateTime, + Expression? microsoftCompletedTimeZone, + Expression? microsoftChecklistItemsJson, + Expression? recurrenceJson, + Expression? importance, + Expression? categoriesJson, + Expression? hasAttachments, + Expression? providerMetadataJson, + Expression? deleted, + Expression? hidden, + Expression? linksJson, + Expression? webViewLink, + Expression? assignmentInfoJson, + Expression? rawJson, + Expression? serverMissing, + Expression? localDirty, + Expression? pendingDelete, + Expression? pendingMove, + Expression? localCreated, + Expression? syncBaseUpdatedUtc, + Expression? lastSyncedAtUtc, + Expression? createdLocalAtUtc, + Expression? updatedLocalAtUtc, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (accountId != null) 'account_id': accountId, + if (taskListId != null) 'task_list_id': taskListId, + if (id != null) 'id': id, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (davObjectId != null) 'dav_object_id': davObjectId, + if (davComponentId != null) 'dav_component_id': davComponentId, + if (icalUid != null) 'ical_uid': icalUid, + if (recurrenceIdKey != null) 'recurrence_id_key': recurrenceIdKey, + if (icalPriority != null) 'ical_priority': icalPriority, + if (percentComplete != null) 'percent_complete': percentComplete, + if (taskLocation != null) 'task_location': taskLocation, + if (taskUrl != null) 'task_url': taskUrl, + if (taskClassification != null) 'task_classification': taskClassification, + if (taskPinned != null) 'task_pinned': taskPinned, + if (taskHideSubtasks != null) 'task_hide_subtasks': taskHideSubtasks, + if (taskHideCompletedSubtasks != null) + 'task_hide_completed_subtasks': taskHideCompletedSubtasks, + if (taskAlarmsJson != null) 'task_alarms_json': taskAlarmsJson, + if (parentUid != null) 'parent_uid': parentUid, + if (sortOrder != null) 'sort_order': sortOrder, + if (providerExtensionProjectionJson != null) + 'provider_extension_projection_json': providerExtensionProjectionJson, + if (projectionVersion != null) 'projection_version': projectionVersion, + if (kind != null) 'kind': kind, + if (etag != null) 'etag': etag, + if (title != null) 'title': title, + if (updatedUtc != null) 'updated_utc': updatedUtc, + if (selfLink != null) 'self_link': selfLink, + if (parent != null) 'parent': parent, + if (position != null) 'position': position, + if (notes != null) 'notes': notes, + if (status != null) 'status': status, + if (dueUtc != null) 'due_utc': dueUtc, + if (completedUtc != null) 'completed_utc': completedUtc, + if (providerStatus != null) 'provider_status': providerStatus, + if (bodyContent != null) 'body_content': bodyContent, + if (bodyContentType != null) 'body_content_type': bodyContentType, + if (microsoftDueDateTime != null) + 'microsoft_due_date_time': microsoftDueDateTime, + if (microsoftDueTimeZone != null) + 'microsoft_due_time_zone': microsoftDueTimeZone, + if (microsoftStartDateTime != null) + 'microsoft_start_date_time': microsoftStartDateTime, + if (microsoftStartTimeZone != null) + 'microsoft_start_time_zone': microsoftStartTimeZone, + if (microsoftReminderDateTime != null) + 'microsoft_reminder_date_time': microsoftReminderDateTime, + if (microsoftReminderTimeZone != null) + 'microsoft_reminder_time_zone': microsoftReminderTimeZone, + if (microsoftIsReminderOn != null) + 'microsoft_is_reminder_on': microsoftIsReminderOn, + if (microsoftCompletedDateTime != null) + 'microsoft_completed_date_time': microsoftCompletedDateTime, + if (microsoftCompletedTimeZone != null) + 'microsoft_completed_time_zone': microsoftCompletedTimeZone, + if (microsoftChecklistItemsJson != null) + 'microsoft_checklist_items_json': microsoftChecklistItemsJson, + if (recurrenceJson != null) 'recurrence_json': recurrenceJson, + if (importance != null) 'importance': importance, + if (categoriesJson != null) 'categories_json': categoriesJson, + if (hasAttachments != null) 'has_attachments': hasAttachments, + if (providerMetadataJson != null) + 'provider_metadata_json': providerMetadataJson, + if (deleted != null) 'deleted': deleted, + if (hidden != null) 'hidden': hidden, + if (linksJson != null) 'links_json': linksJson, + if (webViewLink != null) 'web_view_link': webViewLink, + if (assignmentInfoJson != null) + 'assignment_info_json': assignmentInfoJson, + if (rawJson != null) 'raw_json': rawJson, + if (serverMissing != null) 'server_missing': serverMissing, + if (localDirty != null) 'local_dirty': localDirty, + if (pendingDelete != null) 'pending_delete': pendingDelete, + if (pendingMove != null) 'pending_move': pendingMove, + if (localCreated != null) 'local_created': localCreated, + if (syncBaseUpdatedUtc != null) + 'sync_base_updated_utc': syncBaseUpdatedUtc, + if (lastSyncedAtUtc != null) 'last_synced_at_utc': lastSyncedAtUtc, + if (createdLocalAtUtc != null) 'created_local_at_utc': createdLocalAtUtc, + if (updatedLocalAtUtc != null) 'updated_local_at_utc': updatedLocalAtUtc, + if (rowid != null) 'rowid': rowid, + }); } - Expression calendarEventsRefs( - Expression Function($$CalendarEventsTableFilterComposer f) f, - ) { - final $$CalendarEventsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarEvents, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$CalendarEventsTableFilterComposer( - $db: $db, - $table: $db.calendarEvents, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression calendarSyncStatesRefs( - Expression Function($$CalendarSyncStatesTableFilterComposer f) f, - ) { - final $$CalendarSyncStatesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSyncStates, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$CalendarSyncStatesTableFilterComposer( - $db: $db, - $table: $db.calendarSyncStates, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression scheduleItemOverridesRefs( - Expression Function($$ScheduleItemOverridesTableFilterComposer f) f, - ) { - final $$ScheduleItemOverridesTableFilterComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.scheduleItemOverrides, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$ScheduleItemOverridesTableFilterComposer( - $db: $db, - $table: $db.scheduleItemOverrides, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression notificationScheduleRefs( - Expression Function($$NotificationScheduleTableFilterComposer f) f, - ) { - final $$NotificationScheduleTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.notificationSchedule, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$NotificationScheduleTableFilterComposer( - $db: $db, - $table: $db.notificationSchedule, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), + TasksCompanion copyWith({ + Value? accountId, + Value? taskListId, + Value? id, + Value? davCollectionId, + Value? davObjectId, + Value? davComponentId, + Value? icalUid, + Value? recurrenceIdKey, + Value? icalPriority, + Value? percentComplete, + Value? taskLocation, + Value? taskUrl, + Value? taskClassification, + Value? taskPinned, + Value? taskHideSubtasks, + Value? taskHideCompletedSubtasks, + Value? taskAlarmsJson, + Value? parentUid, + Value? sortOrder, + Value? providerExtensionProjectionJson, + Value? projectionVersion, + Value? kind, + Value? etag, + Value? title, + Value? updatedUtc, + Value? selfLink, + Value? parent, + Value? position, + Value? notes, + Value? status, + Value? dueUtc, + Value? completedUtc, + Value? providerStatus, + Value? bodyContent, + Value? bodyContentType, + Value? microsoftDueDateTime, + Value? microsoftDueTimeZone, + Value? microsoftStartDateTime, + Value? microsoftStartTimeZone, + Value? microsoftReminderDateTime, + Value? microsoftReminderTimeZone, + Value? microsoftIsReminderOn, + Value? microsoftCompletedDateTime, + Value? microsoftCompletedTimeZone, + Value? microsoftChecklistItemsJson, + Value? recurrenceJson, + Value? importance, + Value? categoriesJson, + Value? hasAttachments, + Value? providerMetadataJson, + Value? deleted, + Value? hidden, + Value? linksJson, + Value? webViewLink, + Value? assignmentInfoJson, + Value? rawJson, + Value? serverMissing, + Value? localDirty, + Value? pendingDelete, + Value? pendingMove, + Value? localCreated, + Value? syncBaseUpdatedUtc, + Value? lastSyncedAtUtc, + Value? createdLocalAtUtc, + Value? updatedLocalAtUtc, + Value? rowid, + }) { + return TasksCompanion( + accountId: accountId ?? this.accountId, + taskListId: taskListId ?? this.taskListId, + id: id ?? this.id, + davCollectionId: davCollectionId ?? this.davCollectionId, + davObjectId: davObjectId ?? this.davObjectId, + davComponentId: davComponentId ?? this.davComponentId, + icalUid: icalUid ?? this.icalUid, + recurrenceIdKey: recurrenceIdKey ?? this.recurrenceIdKey, + icalPriority: icalPriority ?? this.icalPriority, + percentComplete: percentComplete ?? this.percentComplete, + taskLocation: taskLocation ?? this.taskLocation, + taskUrl: taskUrl ?? this.taskUrl, + taskClassification: taskClassification ?? this.taskClassification, + taskPinned: taskPinned ?? this.taskPinned, + taskHideSubtasks: taskHideSubtasks ?? this.taskHideSubtasks, + taskHideCompletedSubtasks: + taskHideCompletedSubtasks ?? this.taskHideCompletedSubtasks, + taskAlarmsJson: taskAlarmsJson ?? this.taskAlarmsJson, + parentUid: parentUid ?? this.parentUid, + sortOrder: sortOrder ?? this.sortOrder, + providerExtensionProjectionJson: + providerExtensionProjectionJson ?? + this.providerExtensionProjectionJson, + projectionVersion: projectionVersion ?? this.projectionVersion, + kind: kind ?? this.kind, + etag: etag ?? this.etag, + title: title ?? this.title, + updatedUtc: updatedUtc ?? this.updatedUtc, + selfLink: selfLink ?? this.selfLink, + parent: parent ?? this.parent, + position: position ?? this.position, + notes: notes ?? this.notes, + status: status ?? this.status, + dueUtc: dueUtc ?? this.dueUtc, + completedUtc: completedUtc ?? this.completedUtc, + providerStatus: providerStatus ?? this.providerStatus, + bodyContent: bodyContent ?? this.bodyContent, + bodyContentType: bodyContentType ?? this.bodyContentType, + microsoftDueDateTime: microsoftDueDateTime ?? this.microsoftDueDateTime, + microsoftDueTimeZone: microsoftDueTimeZone ?? this.microsoftDueTimeZone, + microsoftStartDateTime: + microsoftStartDateTime ?? this.microsoftStartDateTime, + microsoftStartTimeZone: + microsoftStartTimeZone ?? this.microsoftStartTimeZone, + microsoftReminderDateTime: + microsoftReminderDateTime ?? this.microsoftReminderDateTime, + microsoftReminderTimeZone: + microsoftReminderTimeZone ?? this.microsoftReminderTimeZone, + microsoftIsReminderOn: + microsoftIsReminderOn ?? this.microsoftIsReminderOn, + microsoftCompletedDateTime: + microsoftCompletedDateTime ?? this.microsoftCompletedDateTime, + microsoftCompletedTimeZone: + microsoftCompletedTimeZone ?? this.microsoftCompletedTimeZone, + microsoftChecklistItemsJson: + microsoftChecklistItemsJson ?? this.microsoftChecklistItemsJson, + recurrenceJson: recurrenceJson ?? this.recurrenceJson, + importance: importance ?? this.importance, + categoriesJson: categoriesJson ?? this.categoriesJson, + hasAttachments: hasAttachments ?? this.hasAttachments, + providerMetadataJson: providerMetadataJson ?? this.providerMetadataJson, + deleted: deleted ?? this.deleted, + hidden: hidden ?? this.hidden, + linksJson: linksJson ?? this.linksJson, + webViewLink: webViewLink ?? this.webViewLink, + assignmentInfoJson: assignmentInfoJson ?? this.assignmentInfoJson, + rawJson: rawJson ?? this.rawJson, + serverMissing: serverMissing ?? this.serverMissing, + localDirty: localDirty ?? this.localDirty, + pendingDelete: pendingDelete ?? this.pendingDelete, + pendingMove: pendingMove ?? this.pendingMove, + localCreated: localCreated ?? this.localCreated, + syncBaseUpdatedUtc: syncBaseUpdatedUtc ?? this.syncBaseUpdatedUtc, + lastSyncedAtUtc: lastSyncedAtUtc ?? this.lastSyncedAtUtc, + createdLocalAtUtc: createdLocalAtUtc ?? this.createdLocalAtUtc, + updatedLocalAtUtc: updatedLocalAtUtc ?? this.updatedLocalAtUtc, + rowid: rowid ?? this.rowid, ); - return f(composer); } -} - -class $$AccountsTableOrderingComposer - extends Composer<_$AppDatabase, $AccountsTable> { - $$AccountsTableOrderingComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get provider => $composableBuilder( - column: $table.provider, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get providerAccountId => $composableBuilder( - column: $table.providerAccountId, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get email => $composableBuilder( - column: $table.email, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get tenantId => $composableBuilder( - column: $table.tenantId, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get accountAvatarUrl => $composableBuilder( - column: $table.accountAvatarUrl, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get providerMetadataJson => $composableBuilder( - column: $table.providerMetadataJson, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get authState => $composableBuilder( - column: $table.authState, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get calendarsEnabled => $composableBuilder( - column: $table.calendarsEnabled, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get tasksEnabled => $composableBuilder( - column: $table.tasksEnabled, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get grantedScopes => $composableBuilder( - column: $table.grantedScopes, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get createdAtUtc => $composableBuilder( - column: $table.createdAtUtc, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get updatedAtUtc => $composableBuilder( - column: $table.updatedAtUtc, - builder: (column) => ColumnOrderings(column), - ); - - ColumnOrderings get lastSuccessfulSyncAtUtc => $composableBuilder( - column: $table.lastSuccessfulSyncAtUtc, - builder: (column) => ColumnOrderings(column), - ); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (taskListId.present) { + map['task_list_id'] = Variable(taskListId.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); + } + if (davObjectId.present) { + map['dav_object_id'] = Variable(davObjectId.value); + } + if (davComponentId.present) { + map['dav_component_id'] = Variable(davComponentId.value); + } + if (icalUid.present) { + map['ical_uid'] = Variable(icalUid.value); + } + if (recurrenceIdKey.present) { + map['recurrence_id_key'] = Variable(recurrenceIdKey.value); + } + if (icalPriority.present) { + map['ical_priority'] = Variable(icalPriority.value); + } + if (percentComplete.present) { + map['percent_complete'] = Variable(percentComplete.value); + } + if (taskLocation.present) { + map['task_location'] = Variable(taskLocation.value); + } + if (taskUrl.present) { + map['task_url'] = Variable(taskUrl.value); + } + if (taskClassification.present) { + map['task_classification'] = Variable(taskClassification.value); + } + if (taskPinned.present) { + map['task_pinned'] = Variable(taskPinned.value); + } + if (taskHideSubtasks.present) { + map['task_hide_subtasks'] = Variable(taskHideSubtasks.value); + } + if (taskHideCompletedSubtasks.present) { + map['task_hide_completed_subtasks'] = Variable( + taskHideCompletedSubtasks.value, + ); + } + if (taskAlarmsJson.present) { + map['task_alarms_json'] = Variable(taskAlarmsJson.value); + } + if (parentUid.present) { + map['parent_uid'] = Variable(parentUid.value); + } + if (sortOrder.present) { + map['sort_order'] = Variable(sortOrder.value); + } + if (providerExtensionProjectionJson.present) { + map['provider_extension_projection_json'] = Variable( + providerExtensionProjectionJson.value, + ); + } + if (projectionVersion.present) { + map['projection_version'] = Variable(projectionVersion.value); + } + if (kind.present) { + map['kind'] = Variable(kind.value); + } + if (etag.present) { + map['etag'] = Variable(etag.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (updatedUtc.present) { + map['updated_utc'] = Variable(updatedUtc.value); + } + if (selfLink.present) { + map['self_link'] = Variable(selfLink.value); + } + if (parent.present) { + map['parent'] = Variable(parent.value); + } + if (position.present) { + map['position'] = Variable(position.value); + } + if (notes.present) { + map['notes'] = Variable(notes.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (dueUtc.present) { + map['due_utc'] = Variable(dueUtc.value); + } + if (completedUtc.present) { + map['completed_utc'] = Variable(completedUtc.value); + } + if (providerStatus.present) { + map['provider_status'] = Variable(providerStatus.value); + } + if (bodyContent.present) { + map['body_content'] = Variable(bodyContent.value); + } + if (bodyContentType.present) { + map['body_content_type'] = Variable(bodyContentType.value); + } + if (microsoftDueDateTime.present) { + map['microsoft_due_date_time'] = Variable( + microsoftDueDateTime.value, + ); + } + if (microsoftDueTimeZone.present) { + map['microsoft_due_time_zone'] = Variable( + microsoftDueTimeZone.value, + ); + } + if (microsoftStartDateTime.present) { + map['microsoft_start_date_time'] = Variable( + microsoftStartDateTime.value, + ); + } + if (microsoftStartTimeZone.present) { + map['microsoft_start_time_zone'] = Variable( + microsoftStartTimeZone.value, + ); + } + if (microsoftReminderDateTime.present) { + map['microsoft_reminder_date_time'] = Variable( + microsoftReminderDateTime.value, + ); + } + if (microsoftReminderTimeZone.present) { + map['microsoft_reminder_time_zone'] = Variable( + microsoftReminderTimeZone.value, + ); + } + if (microsoftIsReminderOn.present) { + map['microsoft_is_reminder_on'] = Variable( + microsoftIsReminderOn.value, + ); + } + if (microsoftCompletedDateTime.present) { + map['microsoft_completed_date_time'] = Variable( + microsoftCompletedDateTime.value, + ); + } + if (microsoftCompletedTimeZone.present) { + map['microsoft_completed_time_zone'] = Variable( + microsoftCompletedTimeZone.value, + ); + } + if (microsoftChecklistItemsJson.present) { + map['microsoft_checklist_items_json'] = Variable( + microsoftChecklistItemsJson.value, + ); + } + if (recurrenceJson.present) { + map['recurrence_json'] = Variable(recurrenceJson.value); + } + if (importance.present) { + map['importance'] = Variable(importance.value); + } + if (categoriesJson.present) { + map['categories_json'] = Variable(categoriesJson.value); + } + if (hasAttachments.present) { + map['has_attachments'] = Variable(hasAttachments.value); + } + if (providerMetadataJson.present) { + map['provider_metadata_json'] = Variable( + providerMetadataJson.value, + ); + } + if (deleted.present) { + map['deleted'] = Variable(deleted.value); + } + if (hidden.present) { + map['hidden'] = Variable(hidden.value); + } + if (linksJson.present) { + map['links_json'] = Variable(linksJson.value); + } + if (webViewLink.present) { + map['web_view_link'] = Variable(webViewLink.value); + } + if (assignmentInfoJson.present) { + map['assignment_info_json'] = Variable(assignmentInfoJson.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (serverMissing.present) { + map['server_missing'] = Variable(serverMissing.value); + } + if (localDirty.present) { + map['local_dirty'] = Variable(localDirty.value); + } + if (pendingDelete.present) { + map['pending_delete'] = Variable(pendingDelete.value); + } + if (pendingMove.present) { + map['pending_move'] = Variable(pendingMove.value); + } + if (localCreated.present) { + map['local_created'] = Variable(localCreated.value); + } + if (syncBaseUpdatedUtc.present) { + map['sync_base_updated_utc'] = Variable(syncBaseUpdatedUtc.value); + } + if (lastSyncedAtUtc.present) { + map['last_synced_at_utc'] = Variable(lastSyncedAtUtc.value); + } + if (createdLocalAtUtc.present) { + map['created_local_at_utc'] = Variable(createdLocalAtUtc.value); + } + if (updatedLocalAtUtc.present) { + map['updated_local_at_utc'] = Variable(updatedLocalAtUtc.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } - ColumnOrderings get lastFullSyncAtUtc => $composableBuilder( - column: $table.lastFullSyncAtUtc, - builder: (column) => ColumnOrderings(column), - ); + @override + String toString() { + return (StringBuffer('TasksCompanion(') + ..write('accountId: $accountId, ') + ..write('taskListId: $taskListId, ') + ..write('id: $id, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('davComponentId: $davComponentId, ') + ..write('icalUid: $icalUid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('icalPriority: $icalPriority, ') + ..write('percentComplete: $percentComplete, ') + ..write('taskLocation: $taskLocation, ') + ..write('taskUrl: $taskUrl, ') + ..write('taskClassification: $taskClassification, ') + ..write('taskPinned: $taskPinned, ') + ..write('taskHideSubtasks: $taskHideSubtasks, ') + ..write('taskHideCompletedSubtasks: $taskHideCompletedSubtasks, ') + ..write('taskAlarmsJson: $taskAlarmsJson, ') + ..write('parentUid: $parentUid, ') + ..write('sortOrder: $sortOrder, ') + ..write( + 'providerExtensionProjectionJson: $providerExtensionProjectionJson, ', + ) + ..write('projectionVersion: $projectionVersion, ') + ..write('kind: $kind, ') + ..write('etag: $etag, ') + ..write('title: $title, ') + ..write('updatedUtc: $updatedUtc, ') + ..write('selfLink: $selfLink, ') + ..write('parent: $parent, ') + ..write('position: $position, ') + ..write('notes: $notes, ') + ..write('status: $status, ') + ..write('dueUtc: $dueUtc, ') + ..write('completedUtc: $completedUtc, ') + ..write('providerStatus: $providerStatus, ') + ..write('bodyContent: $bodyContent, ') + ..write('bodyContentType: $bodyContentType, ') + ..write('microsoftDueDateTime: $microsoftDueDateTime, ') + ..write('microsoftDueTimeZone: $microsoftDueTimeZone, ') + ..write('microsoftStartDateTime: $microsoftStartDateTime, ') + ..write('microsoftStartTimeZone: $microsoftStartTimeZone, ') + ..write('microsoftReminderDateTime: $microsoftReminderDateTime, ') + ..write('microsoftReminderTimeZone: $microsoftReminderTimeZone, ') + ..write('microsoftIsReminderOn: $microsoftIsReminderOn, ') + ..write('microsoftCompletedDateTime: $microsoftCompletedDateTime, ') + ..write('microsoftCompletedTimeZone: $microsoftCompletedTimeZone, ') + ..write('microsoftChecklistItemsJson: $microsoftChecklistItemsJson, ') + ..write('recurrenceJson: $recurrenceJson, ') + ..write('importance: $importance, ') + ..write('categoriesJson: $categoriesJson, ') + ..write('hasAttachments: $hasAttachments, ') + ..write('providerMetadataJson: $providerMetadataJson, ') + ..write('deleted: $deleted, ') + ..write('hidden: $hidden, ') + ..write('linksJson: $linksJson, ') + ..write('webViewLink: $webViewLink, ') + ..write('assignmentInfoJson: $assignmentInfoJson, ') + ..write('rawJson: $rawJson, ') + ..write('serverMissing: $serverMissing, ') + ..write('localDirty: $localDirty, ') + ..write('pendingDelete: $pendingDelete, ') + ..write('pendingMove: $pendingMove, ') + ..write('localCreated: $localCreated, ') + ..write('syncBaseUpdatedUtc: $syncBaseUpdatedUtc, ') + ..write('lastSyncedAtUtc: $lastSyncedAtUtc, ') + ..write('createdLocalAtUtc: $createdLocalAtUtc, ') + ..write('updatedLocalAtUtc: $updatedLocalAtUtc, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } } -class $$AccountsTableAnnotationComposer - extends Composer<_$AppDatabase, $AccountsTable> { - $$AccountsTableAnnotationComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get provider => - $composableBuilder(column: $table.provider, builder: (column) => column); - - GeneratedColumn get providerAccountId => $composableBuilder( - column: $table.providerAccountId, - builder: (column) => column, +class $PendingOpsTable extends PendingOps + with TableInfo<$PendingOpsTable, PendingOp> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PendingOpsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, ); - - GeneratedColumn get displayName => $composableBuilder( - column: $table.displayName, - builder: (column) => column, + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', ); - - GeneratedColumn get email => - $composableBuilder(column: $table.email, builder: (column) => column); - - GeneratedColumn get tenantId => - $composableBuilder(column: $table.tenantId, builder: (column) => column); - - GeneratedColumn get accountAvatarUrl => $composableBuilder( - column: $table.accountAvatarUrl, - builder: (column) => column, + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), ); - - GeneratedColumn get providerMetadataJson => $composableBuilder( - column: $table.providerMetadataJson, - builder: (column) => column, + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', ); - - GeneratedColumn get authState => - $composableBuilder(column: $table.authState, builder: (column) => column); - - GeneratedColumn get calendarsEnabled => $composableBuilder( - column: $table.calendarsEnabled, - builder: (column) => column, + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - - GeneratedColumn get tasksEnabled => $composableBuilder( - column: $table.tasksEnabled, - builder: (column) => column, + static const VerificationMeta _entityTypeMeta = const VerificationMeta( + 'entityType', ); - - GeneratedColumn get grantedScopes => $composableBuilder( - column: $table.grantedScopes, - builder: (column) => column, + @override + late final GeneratedColumn entityType = GeneratedColumn( + 'entity_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, ); - - GeneratedColumn get createdAtUtc => $composableBuilder( - column: $table.createdAtUtc, - builder: (column) => column, + static const VerificationMeta _operationMeta = const VerificationMeta( + 'operation', ); - - GeneratedColumn get updatedAtUtc => $composableBuilder( - column: $table.updatedAtUtc, - builder: (column) => column, + @override + late final GeneratedColumn operation = GeneratedColumn( + 'operation', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, ); - - GeneratedColumn get lastSuccessfulSyncAtUtc => $composableBuilder( - column: $table.lastSuccessfulSyncAtUtc, - builder: (column) => column, + static const VerificationMeta _operationTypeMeta = const VerificationMeta( + 'operationType', ); - - GeneratedColumn get lastFullSyncAtUtc => $composableBuilder( - column: $table.lastFullSyncAtUtc, - builder: (column) => column, + @override + late final GeneratedColumn operationType = GeneratedColumn( + 'operation_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, ); - - Expression taskListsRefs( - Expression Function($$TaskListsTableAnnotationComposer a) f, - ) { - final $$TaskListsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.taskLists, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$TaskListsTableAnnotationComposer( - $db: $db, - $table: $db.taskLists, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression tasksRefs( - Expression Function($$TasksTableAnnotationComposer a) f, - ) { - final $$TasksTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.tasks, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$TasksTableAnnotationComposer( - $db: $db, - $table: $db.tasks, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression pendingOpsRefs( - Expression Function($$PendingOpsTableAnnotationComposer a) f, - ) { - final $$PendingOpsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pendingOps, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$PendingOpsTableAnnotationComposer( - $db: $db, - $table: $db.pendingOps, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression syncRunsRefs( - Expression Function($$SyncRunsTableAnnotationComposer a) f, - ) { - final $$SyncRunsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.syncRuns, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$SyncRunsTableAnnotationComposer( - $db: $db, - $table: $db.syncRuns, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression calendarSourcesRefs( - Expression Function($$CalendarSourcesTableAnnotationComposer a) f, - ) { - final $$CalendarSourcesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSources, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer, - }) => $$CalendarSourcesTableAnnotationComposer( - $db: $db, - $table: $db.calendarSources, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); - return f(composer); - } - - Expression calendarEventsRefs( - Expression Function($$CalendarEventsTableAnnotationComposer a) f, - ) { - final $$CalendarEventsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarEvents, - getReferencedColumn: (t) => t.accountId, - builder: - ( + static const VerificationMeta _taskListIdMeta = const VerificationMeta( + 'taskListId', + ); + @override + late final GeneratedColumn taskListId = GeneratedColumn( + 'task_list_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _taskIdMeta = const VerificationMeta('taskId'); + @override + late final GeneratedColumn taskId = GeneratedColumn( + 'task_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _calendarSourceIdMeta = const VerificationMeta( + 'calendarSourceId', + ); + @override + late final GeneratedColumn calendarSourceId = GeneratedColumn( + 'calendar_source_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _providerCalendarIdMeta = + const VerificationMeta('providerCalendarId'); + @override + late final GeneratedColumn providerCalendarId = + GeneratedColumn( + 'provider_calendar_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _eventIdMeta = const VerificationMeta( + 'eventId', + ); + @override + late final GeneratedColumn eventId = GeneratedColumn( + 'event_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', + ); + @override + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _davCollectionHrefMeta = const VerificationMeta( + 'davCollectionHref', + ); + @override + late final GeneratedColumn davCollectionHref = + GeneratedColumn( + 'dav_collection_href', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _davObjectIdMeta = const VerificationMeta( + 'davObjectId', + ); + @override + late final GeneratedColumn davObjectId = GeneratedColumn( + 'dav_object_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_objects (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _davMemberHrefMeta = const VerificationMeta( + 'davMemberHref', + ); + @override + late final GeneratedColumn davMemberHref = GeneratedColumn( + 'dav_member_href', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _baselineEtagMeta = const VerificationMeta( + 'baselineEtag', + ); + @override + late final GeneratedColumn baselineEtag = GeneratedColumn( + 'baseline_etag', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _baselineRawIcsMeta = const VerificationMeta( + 'baselineRawIcs', + ); + @override + late final GeneratedColumn baselineRawIcs = GeneratedColumn( + 'baseline_raw_ics', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _mutationPatchJsonMeta = const VerificationMeta( + 'mutationPatchJson', + ); + @override + late final GeneratedColumn mutationPatchJson = + GeneratedColumn( + 'mutation_patch_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _mutationPatchSchemaVersionMeta = + const VerificationMeta('mutationPatchSchemaVersion'); + @override + late final GeneratedColumn mutationPatchSchemaVersion = + GeneratedColumn( + 'mutation_patch_schema_version', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _targetComponentKeyMeta = + const VerificationMeta('targetComponentKey'); + @override + late final GeneratedColumn targetComponentKey = + GeneratedColumn( + 'target_component_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _mutationScopeMeta = const VerificationMeta( + 'mutationScope', + ); + @override + late final GeneratedColumn mutationScope = GeneratedColumn( + 'mutation_scope', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _destinationCollectionIdMeta = + const VerificationMeta('destinationCollectionId'); + @override + late final GeneratedColumn destinationCollectionId = + GeneratedColumn( + 'destination_collection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _destinationCollectionHrefMeta = + const VerificationMeta('destinationCollectionHref'); + @override + late final GeneratedColumn destinationCollectionHref = + GeneratedColumn( + 'destination_collection_href', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _destinationMemberHrefMeta = + const VerificationMeta('destinationMemberHref'); + @override + late final GeneratedColumn destinationMemberHref = + GeneratedColumn( + 'destination_member_href', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _conflictStateMeta = const VerificationMeta( + 'conflictState', + ); + @override + late final GeneratedColumn conflictState = GeneratedColumn( + 'conflict_state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _conflictSnapshotIdMeta = + const VerificationMeta('conflictSnapshotId'); + @override + late final GeneratedColumn conflictSnapshotId = + GeneratedColumn( + 'conflict_snapshot_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_conflict_snapshots (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _retryClassificationMeta = + const VerificationMeta('retryClassification'); + @override + late final GeneratedColumn retryClassification = + GeneratedColumn( + 'retry_classification', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _localTempIdMeta = const VerificationMeta( + 'localTempId', + ); + @override + late final GeneratedColumn localTempId = GeneratedColumn( + 'local_temp_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _dependsOnOpIdMeta = const VerificationMeta( + 'dependsOnOpId', + ); + @override + late final GeneratedColumn dependsOnOpId = GeneratedColumn( + 'depends_on_op_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _requestJsonMeta = const VerificationMeta( + 'requestJson', + ); + @override + late final GeneratedColumn requestJson = GeneratedColumn( + 'request_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _baselineUpdatedUtcMeta = + const VerificationMeta('baselineUpdatedUtc'); + @override + late final GeneratedColumn baselineUpdatedUtc = + GeneratedColumn( + 'baseline_updated_utc', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _baselineRawJsonMeta = const VerificationMeta( + 'baselineRawJson', + ); + @override + late final GeneratedColumn baselineRawJson = GeneratedColumn( + 'baseline_raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _attemptCountMeta = const VerificationMeta( + 'attemptCount', + ); + @override + late final GeneratedColumn attemptCount = GeneratedColumn( + 'attempt_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _nextAttemptAtUtcMeta = const VerificationMeta( + 'nextAttemptAtUtc', + ); + @override + late final GeneratedColumn nextAttemptAtUtc = GeneratedColumn( + 'next_attempt_at_utc', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastErrorCodeMeta = const VerificationMeta( + 'lastErrorCode', + ); + @override + late final GeneratedColumn lastErrorCode = GeneratedColumn( + 'last_error_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastErrorMessageMeta = const VerificationMeta( + 'lastErrorMessage', + ); + @override + late final GeneratedColumn lastErrorMessage = GeneratedColumn( + 'last_error_message', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _stateMeta = const VerificationMeta('state'); + @override + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('pending'), + ); + static const VerificationMeta _lastErrorMeta = const VerificationMeta( + 'lastError', + ); + @override + late final GeneratedColumn lastError = GeneratedColumn( + 'last_error', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtUtcMeta = const VerificationMeta( + 'createdAtUtc', + ); + @override + late final GeneratedColumn createdAtUtc = GeneratedColumn( + 'created_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtUtcMeta = const VerificationMeta( + 'updatedAtUtc', + ); + @override + late final GeneratedColumn updatedAtUtc = GeneratedColumn( + 'updated_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + accountId, + provider, + entityType, + operation, + operationType, + taskListId, + taskId, + calendarSourceId, + providerCalendarId, + eventId, + davCollectionId, + davCollectionHref, + davObjectId, + davMemberHref, + baselineEtag, + baselineRawIcs, + mutationPatchJson, + mutationPatchSchemaVersion, + targetComponentKey, + mutationScope, + destinationCollectionId, + destinationCollectionHref, + destinationMemberHref, + conflictState, + conflictSnapshotId, + retryClassification, + localTempId, + dependsOnOpId, + requestJson, + baselineUpdatedUtc, + baselineRawJson, + attemptCount, + nextAttemptAtUtc, + lastErrorCode, + lastErrorMessage, + state, + lastError, + createdAtUtc, + updatedAtUtc, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'pending_ops'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } + if (data.containsKey('entity_type')) { + context.handle( + _entityTypeMeta, + entityType.isAcceptableOrUnknown(data['entity_type']!, _entityTypeMeta), + ); + } else if (isInserting) { + context.missing(_entityTypeMeta); + } + if (data.containsKey('operation')) { + context.handle( + _operationMeta, + operation.isAcceptableOrUnknown(data['operation']!, _operationMeta), + ); + } else if (isInserting) { + context.missing(_operationMeta); + } + if (data.containsKey('operation_type')) { + context.handle( + _operationTypeMeta, + operationType.isAcceptableOrUnknown( + data['operation_type']!, + _operationTypeMeta, + ), + ); + } + if (data.containsKey('task_list_id')) { + context.handle( + _taskListIdMeta, + taskListId.isAcceptableOrUnknown( + data['task_list_id']!, + _taskListIdMeta, + ), + ); + } + if (data.containsKey('task_id')) { + context.handle( + _taskIdMeta, + taskId.isAcceptableOrUnknown(data['task_id']!, _taskIdMeta), + ); + } + if (data.containsKey('calendar_source_id')) { + context.handle( + _calendarSourceIdMeta, + calendarSourceId.isAcceptableOrUnknown( + data['calendar_source_id']!, + _calendarSourceIdMeta, + ), + ); + } + if (data.containsKey('provider_calendar_id')) { + context.handle( + _providerCalendarIdMeta, + providerCalendarId.isAcceptableOrUnknown( + data['provider_calendar_id']!, + _providerCalendarIdMeta, + ), + ); + } + if (data.containsKey('event_id')) { + context.handle( + _eventIdMeta, + eventId.isAcceptableOrUnknown(data['event_id']!, _eventIdMeta), + ); + } + if (data.containsKey('dav_collection_id')) { + context.handle( + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), + ); + } + if (data.containsKey('dav_collection_href')) { + context.handle( + _davCollectionHrefMeta, + davCollectionHref.isAcceptableOrUnknown( + data['dav_collection_href']!, + _davCollectionHrefMeta, + ), + ); + } + if (data.containsKey('dav_object_id')) { + context.handle( + _davObjectIdMeta, + davObjectId.isAcceptableOrUnknown( + data['dav_object_id']!, + _davObjectIdMeta, + ), + ); + } + if (data.containsKey('dav_member_href')) { + context.handle( + _davMemberHrefMeta, + davMemberHref.isAcceptableOrUnknown( + data['dav_member_href']!, + _davMemberHrefMeta, + ), + ); + } + if (data.containsKey('baseline_etag')) { + context.handle( + _baselineEtagMeta, + baselineEtag.isAcceptableOrUnknown( + data['baseline_etag']!, + _baselineEtagMeta, + ), + ); + } + if (data.containsKey('baseline_raw_ics')) { + context.handle( + _baselineRawIcsMeta, + baselineRawIcs.isAcceptableOrUnknown( + data['baseline_raw_ics']!, + _baselineRawIcsMeta, + ), + ); + } + if (data.containsKey('mutation_patch_json')) { + context.handle( + _mutationPatchJsonMeta, + mutationPatchJson.isAcceptableOrUnknown( + data['mutation_patch_json']!, + _mutationPatchJsonMeta, + ), + ); + } + if (data.containsKey('mutation_patch_schema_version')) { + context.handle( + _mutationPatchSchemaVersionMeta, + mutationPatchSchemaVersion.isAcceptableOrUnknown( + data['mutation_patch_schema_version']!, + _mutationPatchSchemaVersionMeta, + ), + ); + } + if (data.containsKey('target_component_key')) { + context.handle( + _targetComponentKeyMeta, + targetComponentKey.isAcceptableOrUnknown( + data['target_component_key']!, + _targetComponentKeyMeta, + ), + ); + } + if (data.containsKey('mutation_scope')) { + context.handle( + _mutationScopeMeta, + mutationScope.isAcceptableOrUnknown( + data['mutation_scope']!, + _mutationScopeMeta, + ), + ); + } + if (data.containsKey('destination_collection_id')) { + context.handle( + _destinationCollectionIdMeta, + destinationCollectionId.isAcceptableOrUnknown( + data['destination_collection_id']!, + _destinationCollectionIdMeta, + ), + ); + } + if (data.containsKey('destination_collection_href')) { + context.handle( + _destinationCollectionHrefMeta, + destinationCollectionHref.isAcceptableOrUnknown( + data['destination_collection_href']!, + _destinationCollectionHrefMeta, + ), + ); + } + if (data.containsKey('destination_member_href')) { + context.handle( + _destinationMemberHrefMeta, + destinationMemberHref.isAcceptableOrUnknown( + data['destination_member_href']!, + _destinationMemberHrefMeta, + ), + ); + } + if (data.containsKey('conflict_state')) { + context.handle( + _conflictStateMeta, + conflictState.isAcceptableOrUnknown( + data['conflict_state']!, + _conflictStateMeta, + ), + ); + } + if (data.containsKey('conflict_snapshot_id')) { + context.handle( + _conflictSnapshotIdMeta, + conflictSnapshotId.isAcceptableOrUnknown( + data['conflict_snapshot_id']!, + _conflictSnapshotIdMeta, + ), + ); + } + if (data.containsKey('retry_classification')) { + context.handle( + _retryClassificationMeta, + retryClassification.isAcceptableOrUnknown( + data['retry_classification']!, + _retryClassificationMeta, + ), + ); + } + if (data.containsKey('local_temp_id')) { + context.handle( + _localTempIdMeta, + localTempId.isAcceptableOrUnknown( + data['local_temp_id']!, + _localTempIdMeta, + ), + ); + } + if (data.containsKey('depends_on_op_id')) { + context.handle( + _dependsOnOpIdMeta, + dependsOnOpId.isAcceptableOrUnknown( + data['depends_on_op_id']!, + _dependsOnOpIdMeta, + ), + ); + } + if (data.containsKey('request_json')) { + context.handle( + _requestJsonMeta, + requestJson.isAcceptableOrUnknown( + data['request_json']!, + _requestJsonMeta, + ), + ); + } else if (isInserting) { + context.missing(_requestJsonMeta); + } + if (data.containsKey('baseline_updated_utc')) { + context.handle( + _baselineUpdatedUtcMeta, + baselineUpdatedUtc.isAcceptableOrUnknown( + data['baseline_updated_utc']!, + _baselineUpdatedUtcMeta, + ), + ); + } + if (data.containsKey('baseline_raw_json')) { + context.handle( + _baselineRawJsonMeta, + baselineRawJson.isAcceptableOrUnknown( + data['baseline_raw_json']!, + _baselineRawJsonMeta, + ), + ); + } + if (data.containsKey('attempt_count')) { + context.handle( + _attemptCountMeta, + attemptCount.isAcceptableOrUnknown( + data['attempt_count']!, + _attemptCountMeta, + ), + ); + } + if (data.containsKey('next_attempt_at_utc')) { + context.handle( + _nextAttemptAtUtcMeta, + nextAttemptAtUtc.isAcceptableOrUnknown( + data['next_attempt_at_utc']!, + _nextAttemptAtUtcMeta, + ), + ); + } + if (data.containsKey('last_error_code')) { + context.handle( + _lastErrorCodeMeta, + lastErrorCode.isAcceptableOrUnknown( + data['last_error_code']!, + _lastErrorCodeMeta, + ), + ); + } + if (data.containsKey('last_error_message')) { + context.handle( + _lastErrorMessageMeta, + lastErrorMessage.isAcceptableOrUnknown( + data['last_error_message']!, + _lastErrorMessageMeta, + ), + ); + } + if (data.containsKey('state')) { + context.handle( + _stateMeta, + state.isAcceptableOrUnknown(data['state']!, _stateMeta), + ); + } + if (data.containsKey('last_error')) { + context.handle( + _lastErrorMeta, + lastError.isAcceptableOrUnknown(data['last_error']!, _lastErrorMeta), + ); + } + if (data.containsKey('created_at_utc')) { + context.handle( + _createdAtUtcMeta, + createdAtUtc.isAcceptableOrUnknown( + data['created_at_utc']!, + _createdAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdAtUtcMeta); + } + if (data.containsKey('updated_at_utc')) { + context.handle( + _updatedAtUtcMeta, + updatedAtUtc.isAcceptableOrUnknown( + data['updated_at_utc']!, + _updatedAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedAtUtcMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PendingOp map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PendingOp( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + ), + entityType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}entity_type'], + )!, + operation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}operation'], + )!, + operationType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}operation_type'], + ), + taskListId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_list_id'], + ), + taskId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}task_id'], + ), + calendarSourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}calendar_source_id'], + ), + providerCalendarId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_calendar_id'], + ), + eventId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}event_id'], + ), + davCollectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_collection_id'], + ), + davCollectionHref: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_collection_href'], + ), + davObjectId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_object_id'], + ), + davMemberHref: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_member_href'], + ), + baselineEtag: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_etag'], + ), + baselineRawIcs: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_raw_ics'], + ), + mutationPatchJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mutation_patch_json'], + ), + mutationPatchSchemaVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mutation_patch_schema_version'], + ), + targetComponentKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}target_component_key'], + ), + mutationScope: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mutation_scope'], + ), + destinationCollectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destination_collection_id'], + ), + destinationCollectionHref: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destination_collection_href'], + ), + destinationMemberHref: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}destination_member_href'], + ), + conflictState: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conflict_state'], + ), + conflictSnapshotId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conflict_snapshot_id'], + ), + retryClassification: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}retry_classification'], + ), + localTempId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}local_temp_id'], + ), + dependsOnOpId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}depends_on_op_id'], + ), + requestJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}request_json'], + )!, + baselineUpdatedUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_updated_utc'], + ), + baselineRawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_raw_json'], + ), + attemptCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}attempt_count'], + )!, + nextAttemptAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}next_attempt_at_utc'], + ), + lastErrorCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error_code'], + ), + lastErrorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error_message'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + )!, + lastError: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_error'], + ), + createdAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_utc'], + )!, + updatedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at_utc'], + )!, + ); + } + + @override + $PendingOpsTable createAlias(String alias) { + return $PendingOpsTable(attachedDatabase, alias); + } +} + +class PendingOp extends DataClass implements Insertable { + final String id; + final String accountId; + final String? provider; + final String entityType; + final String operation; + final String? operationType; + final String? taskListId; + final String? taskId; + final String? calendarSourceId; + final String? providerCalendarId; + final String? eventId; + final String? davCollectionId; + final String? davCollectionHref; + final String? davObjectId; + final String? davMemberHref; + final String? baselineEtag; + final String? baselineRawIcs; + final String? mutationPatchJson; + final int? mutationPatchSchemaVersion; + final String? targetComponentKey; + final String? mutationScope; + final String? destinationCollectionId; + final String? destinationCollectionHref; + final String? destinationMemberHref; + final String? conflictState; + final String? conflictSnapshotId; + final String? retryClassification; + final String? localTempId; + final String? dependsOnOpId; + final String requestJson; + final String? baselineUpdatedUtc; + final String? baselineRawJson; + final int attemptCount; + final String? nextAttemptAtUtc; + final String? lastErrorCode; + final String? lastErrorMessage; + final String state; + final String? lastError; + final String createdAtUtc; + final String updatedAtUtc; + const PendingOp({ + required this.id, + required this.accountId, + this.provider, + required this.entityType, + required this.operation, + this.operationType, + this.taskListId, + this.taskId, + this.calendarSourceId, + this.providerCalendarId, + this.eventId, + this.davCollectionId, + this.davCollectionHref, + this.davObjectId, + this.davMemberHref, + this.baselineEtag, + this.baselineRawIcs, + this.mutationPatchJson, + this.mutationPatchSchemaVersion, + this.targetComponentKey, + this.mutationScope, + this.destinationCollectionId, + this.destinationCollectionHref, + this.destinationMemberHref, + this.conflictState, + this.conflictSnapshotId, + this.retryClassification, + this.localTempId, + this.dependsOnOpId, + required this.requestJson, + this.baselineUpdatedUtc, + this.baselineRawJson, + required this.attemptCount, + this.nextAttemptAtUtc, + this.lastErrorCode, + this.lastErrorMessage, + required this.state, + this.lastError, + required this.createdAtUtc, + required this.updatedAtUtc, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + if (!nullToAbsent || provider != null) { + map['provider'] = Variable(provider); + } + map['entity_type'] = Variable(entityType); + map['operation'] = Variable(operation); + if (!nullToAbsent || operationType != null) { + map['operation_type'] = Variable(operationType); + } + if (!nullToAbsent || taskListId != null) { + map['task_list_id'] = Variable(taskListId); + } + if (!nullToAbsent || taskId != null) { + map['task_id'] = Variable(taskId); + } + if (!nullToAbsent || calendarSourceId != null) { + map['calendar_source_id'] = Variable(calendarSourceId); + } + if (!nullToAbsent || providerCalendarId != null) { + map['provider_calendar_id'] = Variable(providerCalendarId); + } + if (!nullToAbsent || eventId != null) { + map['event_id'] = Variable(eventId); + } + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); + } + if (!nullToAbsent || davCollectionHref != null) { + map['dav_collection_href'] = Variable(davCollectionHref); + } + if (!nullToAbsent || davObjectId != null) { + map['dav_object_id'] = Variable(davObjectId); + } + if (!nullToAbsent || davMemberHref != null) { + map['dav_member_href'] = Variable(davMemberHref); + } + if (!nullToAbsent || baselineEtag != null) { + map['baseline_etag'] = Variable(baselineEtag); + } + if (!nullToAbsent || baselineRawIcs != null) { + map['baseline_raw_ics'] = Variable(baselineRawIcs); + } + if (!nullToAbsent || mutationPatchJson != null) { + map['mutation_patch_json'] = Variable(mutationPatchJson); + } + if (!nullToAbsent || mutationPatchSchemaVersion != null) { + map['mutation_patch_schema_version'] = Variable( + mutationPatchSchemaVersion, + ); + } + if (!nullToAbsent || targetComponentKey != null) { + map['target_component_key'] = Variable(targetComponentKey); + } + if (!nullToAbsent || mutationScope != null) { + map['mutation_scope'] = Variable(mutationScope); + } + if (!nullToAbsent || destinationCollectionId != null) { + map['destination_collection_id'] = Variable( + destinationCollectionId, + ); + } + if (!nullToAbsent || destinationCollectionHref != null) { + map['destination_collection_href'] = Variable( + destinationCollectionHref, + ); + } + if (!nullToAbsent || destinationMemberHref != null) { + map['destination_member_href'] = Variable(destinationMemberHref); + } + if (!nullToAbsent || conflictState != null) { + map['conflict_state'] = Variable(conflictState); + } + if (!nullToAbsent || conflictSnapshotId != null) { + map['conflict_snapshot_id'] = Variable(conflictSnapshotId); + } + if (!nullToAbsent || retryClassification != null) { + map['retry_classification'] = Variable(retryClassification); + } + if (!nullToAbsent || localTempId != null) { + map['local_temp_id'] = Variable(localTempId); + } + if (!nullToAbsent || dependsOnOpId != null) { + map['depends_on_op_id'] = Variable(dependsOnOpId); + } + map['request_json'] = Variable(requestJson); + if (!nullToAbsent || baselineUpdatedUtc != null) { + map['baseline_updated_utc'] = Variable(baselineUpdatedUtc); + } + if (!nullToAbsent || baselineRawJson != null) { + map['baseline_raw_json'] = Variable(baselineRawJson); + } + map['attempt_count'] = Variable(attemptCount); + if (!nullToAbsent || nextAttemptAtUtc != null) { + map['next_attempt_at_utc'] = Variable(nextAttemptAtUtc); + } + if (!nullToAbsent || lastErrorCode != null) { + map['last_error_code'] = Variable(lastErrorCode); + } + if (!nullToAbsent || lastErrorMessage != null) { + map['last_error_message'] = Variable(lastErrorMessage); + } + map['state'] = Variable(state); + if (!nullToAbsent || lastError != null) { + map['last_error'] = Variable(lastError); + } + map['created_at_utc'] = Variable(createdAtUtc); + map['updated_at_utc'] = Variable(updatedAtUtc); + return map; + } + + PendingOpsCompanion toCompanion(bool nullToAbsent) { + return PendingOpsCompanion( + id: Value(id), + accountId: Value(accountId), + provider: provider == null && nullToAbsent + ? const Value.absent() + : Value(provider), + entityType: Value(entityType), + operation: Value(operation), + operationType: operationType == null && nullToAbsent + ? const Value.absent() + : Value(operationType), + taskListId: taskListId == null && nullToAbsent + ? const Value.absent() + : Value(taskListId), + taskId: taskId == null && nullToAbsent + ? const Value.absent() + : Value(taskId), + calendarSourceId: calendarSourceId == null && nullToAbsent + ? const Value.absent() + : Value(calendarSourceId), + providerCalendarId: providerCalendarId == null && nullToAbsent + ? const Value.absent() + : Value(providerCalendarId), + eventId: eventId == null && nullToAbsent + ? const Value.absent() + : Value(eventId), + davCollectionId: davCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionId), + davCollectionHref: davCollectionHref == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionHref), + davObjectId: davObjectId == null && nullToAbsent + ? const Value.absent() + : Value(davObjectId), + davMemberHref: davMemberHref == null && nullToAbsent + ? const Value.absent() + : Value(davMemberHref), + baselineEtag: baselineEtag == null && nullToAbsent + ? const Value.absent() + : Value(baselineEtag), + baselineRawIcs: baselineRawIcs == null && nullToAbsent + ? const Value.absent() + : Value(baselineRawIcs), + mutationPatchJson: mutationPatchJson == null && nullToAbsent + ? const Value.absent() + : Value(mutationPatchJson), + mutationPatchSchemaVersion: + mutationPatchSchemaVersion == null && nullToAbsent + ? const Value.absent() + : Value(mutationPatchSchemaVersion), + targetComponentKey: targetComponentKey == null && nullToAbsent + ? const Value.absent() + : Value(targetComponentKey), + mutationScope: mutationScope == null && nullToAbsent + ? const Value.absent() + : Value(mutationScope), + destinationCollectionId: destinationCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(destinationCollectionId), + destinationCollectionHref: + destinationCollectionHref == null && nullToAbsent + ? const Value.absent() + : Value(destinationCollectionHref), + destinationMemberHref: destinationMemberHref == null && nullToAbsent + ? const Value.absent() + : Value(destinationMemberHref), + conflictState: conflictState == null && nullToAbsent + ? const Value.absent() + : Value(conflictState), + conflictSnapshotId: conflictSnapshotId == null && nullToAbsent + ? const Value.absent() + : Value(conflictSnapshotId), + retryClassification: retryClassification == null && nullToAbsent + ? const Value.absent() + : Value(retryClassification), + localTempId: localTempId == null && nullToAbsent + ? const Value.absent() + : Value(localTempId), + dependsOnOpId: dependsOnOpId == null && nullToAbsent + ? const Value.absent() + : Value(dependsOnOpId), + requestJson: Value(requestJson), + baselineUpdatedUtc: baselineUpdatedUtc == null && nullToAbsent + ? const Value.absent() + : Value(baselineUpdatedUtc), + baselineRawJson: baselineRawJson == null && nullToAbsent + ? const Value.absent() + : Value(baselineRawJson), + attemptCount: Value(attemptCount), + nextAttemptAtUtc: nextAttemptAtUtc == null && nullToAbsent + ? const Value.absent() + : Value(nextAttemptAtUtc), + lastErrorCode: lastErrorCode == null && nullToAbsent + ? const Value.absent() + : Value(lastErrorCode), + lastErrorMessage: lastErrorMessage == null && nullToAbsent + ? const Value.absent() + : Value(lastErrorMessage), + state: Value(state), + lastError: lastError == null && nullToAbsent + ? const Value.absent() + : Value(lastError), + createdAtUtc: Value(createdAtUtc), + updatedAtUtc: Value(updatedAtUtc), + ); + } + + factory PendingOp.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PendingOp( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + provider: serializer.fromJson(json['provider']), + entityType: serializer.fromJson(json['entityType']), + operation: serializer.fromJson(json['operation']), + operationType: serializer.fromJson(json['operationType']), + taskListId: serializer.fromJson(json['taskListId']), + taskId: serializer.fromJson(json['taskId']), + calendarSourceId: serializer.fromJson(json['calendarSourceId']), + providerCalendarId: serializer.fromJson( + json['providerCalendarId'], + ), + eventId: serializer.fromJson(json['eventId']), + davCollectionId: serializer.fromJson(json['davCollectionId']), + davCollectionHref: serializer.fromJson( + json['davCollectionHref'], + ), + davObjectId: serializer.fromJson(json['davObjectId']), + davMemberHref: serializer.fromJson(json['davMemberHref']), + baselineEtag: serializer.fromJson(json['baselineEtag']), + baselineRawIcs: serializer.fromJson(json['baselineRawIcs']), + mutationPatchJson: serializer.fromJson( + json['mutationPatchJson'], + ), + mutationPatchSchemaVersion: serializer.fromJson( + json['mutationPatchSchemaVersion'], + ), + targetComponentKey: serializer.fromJson( + json['targetComponentKey'], + ), + mutationScope: serializer.fromJson(json['mutationScope']), + destinationCollectionId: serializer.fromJson( + json['destinationCollectionId'], + ), + destinationCollectionHref: serializer.fromJson( + json['destinationCollectionHref'], + ), + destinationMemberHref: serializer.fromJson( + json['destinationMemberHref'], + ), + conflictState: serializer.fromJson(json['conflictState']), + conflictSnapshotId: serializer.fromJson( + json['conflictSnapshotId'], + ), + retryClassification: serializer.fromJson( + json['retryClassification'], + ), + localTempId: serializer.fromJson(json['localTempId']), + dependsOnOpId: serializer.fromJson(json['dependsOnOpId']), + requestJson: serializer.fromJson(json['requestJson']), + baselineUpdatedUtc: serializer.fromJson( + json['baselineUpdatedUtc'], + ), + baselineRawJson: serializer.fromJson(json['baselineRawJson']), + attemptCount: serializer.fromJson(json['attemptCount']), + nextAttemptAtUtc: serializer.fromJson(json['nextAttemptAtUtc']), + lastErrorCode: serializer.fromJson(json['lastErrorCode']), + lastErrorMessage: serializer.fromJson(json['lastErrorMessage']), + state: serializer.fromJson(json['state']), + lastError: serializer.fromJson(json['lastError']), + createdAtUtc: serializer.fromJson(json['createdAtUtc']), + updatedAtUtc: serializer.fromJson(json['updatedAtUtc']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'provider': serializer.toJson(provider), + 'entityType': serializer.toJson(entityType), + 'operation': serializer.toJson(operation), + 'operationType': serializer.toJson(operationType), + 'taskListId': serializer.toJson(taskListId), + 'taskId': serializer.toJson(taskId), + 'calendarSourceId': serializer.toJson(calendarSourceId), + 'providerCalendarId': serializer.toJson(providerCalendarId), + 'eventId': serializer.toJson(eventId), + 'davCollectionId': serializer.toJson(davCollectionId), + 'davCollectionHref': serializer.toJson(davCollectionHref), + 'davObjectId': serializer.toJson(davObjectId), + 'davMemberHref': serializer.toJson(davMemberHref), + 'baselineEtag': serializer.toJson(baselineEtag), + 'baselineRawIcs': serializer.toJson(baselineRawIcs), + 'mutationPatchJson': serializer.toJson(mutationPatchJson), + 'mutationPatchSchemaVersion': serializer.toJson( + mutationPatchSchemaVersion, + ), + 'targetComponentKey': serializer.toJson(targetComponentKey), + 'mutationScope': serializer.toJson(mutationScope), + 'destinationCollectionId': serializer.toJson( + destinationCollectionId, + ), + 'destinationCollectionHref': serializer.toJson( + destinationCollectionHref, + ), + 'destinationMemberHref': serializer.toJson( + destinationMemberHref, + ), + 'conflictState': serializer.toJson(conflictState), + 'conflictSnapshotId': serializer.toJson(conflictSnapshotId), + 'retryClassification': serializer.toJson(retryClassification), + 'localTempId': serializer.toJson(localTempId), + 'dependsOnOpId': serializer.toJson(dependsOnOpId), + 'requestJson': serializer.toJson(requestJson), + 'baselineUpdatedUtc': serializer.toJson(baselineUpdatedUtc), + 'baselineRawJson': serializer.toJson(baselineRawJson), + 'attemptCount': serializer.toJson(attemptCount), + 'nextAttemptAtUtc': serializer.toJson(nextAttemptAtUtc), + 'lastErrorCode': serializer.toJson(lastErrorCode), + 'lastErrorMessage': serializer.toJson(lastErrorMessage), + 'state': serializer.toJson(state), + 'lastError': serializer.toJson(lastError), + 'createdAtUtc': serializer.toJson(createdAtUtc), + 'updatedAtUtc': serializer.toJson(updatedAtUtc), + }; + } + + PendingOp copyWith({ + String? id, + String? accountId, + Value provider = const Value.absent(), + String? entityType, + String? operation, + Value operationType = const Value.absent(), + Value taskListId = const Value.absent(), + Value taskId = const Value.absent(), + Value calendarSourceId = const Value.absent(), + Value providerCalendarId = const Value.absent(), + Value eventId = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davCollectionHref = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davMemberHref = const Value.absent(), + Value baselineEtag = const Value.absent(), + Value baselineRawIcs = const Value.absent(), + Value mutationPatchJson = const Value.absent(), + Value mutationPatchSchemaVersion = const Value.absent(), + Value targetComponentKey = const Value.absent(), + Value mutationScope = const Value.absent(), + Value destinationCollectionId = const Value.absent(), + Value destinationCollectionHref = const Value.absent(), + Value destinationMemberHref = const Value.absent(), + Value conflictState = const Value.absent(), + Value conflictSnapshotId = const Value.absent(), + Value retryClassification = const Value.absent(), + Value localTempId = const Value.absent(), + Value dependsOnOpId = const Value.absent(), + String? requestJson, + Value baselineUpdatedUtc = const Value.absent(), + Value baselineRawJson = const Value.absent(), + int? attemptCount, + Value nextAttemptAtUtc = const Value.absent(), + Value lastErrorCode = const Value.absent(), + Value lastErrorMessage = const Value.absent(), + String? state, + Value lastError = const Value.absent(), + String? createdAtUtc, + String? updatedAtUtc, + }) => PendingOp( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider.present ? provider.value : this.provider, + entityType: entityType ?? this.entityType, + operation: operation ?? this.operation, + operationType: operationType.present + ? operationType.value + : this.operationType, + taskListId: taskListId.present ? taskListId.value : this.taskListId, + taskId: taskId.present ? taskId.value : this.taskId, + calendarSourceId: calendarSourceId.present + ? calendarSourceId.value + : this.calendarSourceId, + providerCalendarId: providerCalendarId.present + ? providerCalendarId.value + : this.providerCalendarId, + eventId: eventId.present ? eventId.value : this.eventId, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + davCollectionHref: davCollectionHref.present + ? davCollectionHref.value + : this.davCollectionHref, + davObjectId: davObjectId.present ? davObjectId.value : this.davObjectId, + davMemberHref: davMemberHref.present + ? davMemberHref.value + : this.davMemberHref, + baselineEtag: baselineEtag.present ? baselineEtag.value : this.baselineEtag, + baselineRawIcs: baselineRawIcs.present + ? baselineRawIcs.value + : this.baselineRawIcs, + mutationPatchJson: mutationPatchJson.present + ? mutationPatchJson.value + : this.mutationPatchJson, + mutationPatchSchemaVersion: mutationPatchSchemaVersion.present + ? mutationPatchSchemaVersion.value + : this.mutationPatchSchemaVersion, + targetComponentKey: targetComponentKey.present + ? targetComponentKey.value + : this.targetComponentKey, + mutationScope: mutationScope.present + ? mutationScope.value + : this.mutationScope, + destinationCollectionId: destinationCollectionId.present + ? destinationCollectionId.value + : this.destinationCollectionId, + destinationCollectionHref: destinationCollectionHref.present + ? destinationCollectionHref.value + : this.destinationCollectionHref, + destinationMemberHref: destinationMemberHref.present + ? destinationMemberHref.value + : this.destinationMemberHref, + conflictState: conflictState.present + ? conflictState.value + : this.conflictState, + conflictSnapshotId: conflictSnapshotId.present + ? conflictSnapshotId.value + : this.conflictSnapshotId, + retryClassification: retryClassification.present + ? retryClassification.value + : this.retryClassification, + localTempId: localTempId.present ? localTempId.value : this.localTempId, + dependsOnOpId: dependsOnOpId.present + ? dependsOnOpId.value + : this.dependsOnOpId, + requestJson: requestJson ?? this.requestJson, + baselineUpdatedUtc: baselineUpdatedUtc.present + ? baselineUpdatedUtc.value + : this.baselineUpdatedUtc, + baselineRawJson: baselineRawJson.present + ? baselineRawJson.value + : this.baselineRawJson, + attemptCount: attemptCount ?? this.attemptCount, + nextAttemptAtUtc: nextAttemptAtUtc.present + ? nextAttemptAtUtc.value + : this.nextAttemptAtUtc, + lastErrorCode: lastErrorCode.present + ? lastErrorCode.value + : this.lastErrorCode, + lastErrorMessage: lastErrorMessage.present + ? lastErrorMessage.value + : this.lastErrorMessage, + state: state ?? this.state, + lastError: lastError.present ? lastError.value : this.lastError, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + ); + PendingOp copyWithCompanion(PendingOpsCompanion data) { + return PendingOp( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + provider: data.provider.present ? data.provider.value : this.provider, + entityType: data.entityType.present + ? data.entityType.value + : this.entityType, + operation: data.operation.present ? data.operation.value : this.operation, + operationType: data.operationType.present + ? data.operationType.value + : this.operationType, + taskListId: data.taskListId.present + ? data.taskListId.value + : this.taskListId, + taskId: data.taskId.present ? data.taskId.value : this.taskId, + calendarSourceId: data.calendarSourceId.present + ? data.calendarSourceId.value + : this.calendarSourceId, + providerCalendarId: data.providerCalendarId.present + ? data.providerCalendarId.value + : this.providerCalendarId, + eventId: data.eventId.present ? data.eventId.value : this.eventId, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + davCollectionHref: data.davCollectionHref.present + ? data.davCollectionHref.value + : this.davCollectionHref, + davObjectId: data.davObjectId.present + ? data.davObjectId.value + : this.davObjectId, + davMemberHref: data.davMemberHref.present + ? data.davMemberHref.value + : this.davMemberHref, + baselineEtag: data.baselineEtag.present + ? data.baselineEtag.value + : this.baselineEtag, + baselineRawIcs: data.baselineRawIcs.present + ? data.baselineRawIcs.value + : this.baselineRawIcs, + mutationPatchJson: data.mutationPatchJson.present + ? data.mutationPatchJson.value + : this.mutationPatchJson, + mutationPatchSchemaVersion: data.mutationPatchSchemaVersion.present + ? data.mutationPatchSchemaVersion.value + : this.mutationPatchSchemaVersion, + targetComponentKey: data.targetComponentKey.present + ? data.targetComponentKey.value + : this.targetComponentKey, + mutationScope: data.mutationScope.present + ? data.mutationScope.value + : this.mutationScope, + destinationCollectionId: data.destinationCollectionId.present + ? data.destinationCollectionId.value + : this.destinationCollectionId, + destinationCollectionHref: data.destinationCollectionHref.present + ? data.destinationCollectionHref.value + : this.destinationCollectionHref, + destinationMemberHref: data.destinationMemberHref.present + ? data.destinationMemberHref.value + : this.destinationMemberHref, + conflictState: data.conflictState.present + ? data.conflictState.value + : this.conflictState, + conflictSnapshotId: data.conflictSnapshotId.present + ? data.conflictSnapshotId.value + : this.conflictSnapshotId, + retryClassification: data.retryClassification.present + ? data.retryClassification.value + : this.retryClassification, + localTempId: data.localTempId.present + ? data.localTempId.value + : this.localTempId, + dependsOnOpId: data.dependsOnOpId.present + ? data.dependsOnOpId.value + : this.dependsOnOpId, + requestJson: data.requestJson.present + ? data.requestJson.value + : this.requestJson, + baselineUpdatedUtc: data.baselineUpdatedUtc.present + ? data.baselineUpdatedUtc.value + : this.baselineUpdatedUtc, + baselineRawJson: data.baselineRawJson.present + ? data.baselineRawJson.value + : this.baselineRawJson, + attemptCount: data.attemptCount.present + ? data.attemptCount.value + : this.attemptCount, + nextAttemptAtUtc: data.nextAttemptAtUtc.present + ? data.nextAttemptAtUtc.value + : this.nextAttemptAtUtc, + lastErrorCode: data.lastErrorCode.present + ? data.lastErrorCode.value + : this.lastErrorCode, + lastErrorMessage: data.lastErrorMessage.present + ? data.lastErrorMessage.value + : this.lastErrorMessage, + state: data.state.present ? data.state.value : this.state, + lastError: data.lastError.present ? data.lastError.value : this.lastError, + createdAtUtc: data.createdAtUtc.present + ? data.createdAtUtc.value + : this.createdAtUtc, + updatedAtUtc: data.updatedAtUtc.present + ? data.updatedAtUtc.value + : this.updatedAtUtc, + ); + } + + @override + String toString() { + return (StringBuffer('PendingOp(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('entityType: $entityType, ') + ..write('operation: $operation, ') + ..write('operationType: $operationType, ') + ..write('taskListId: $taskListId, ') + ..write('taskId: $taskId, ') + ..write('calendarSourceId: $calendarSourceId, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('eventId: $eventId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davCollectionHref: $davCollectionHref, ') + ..write('davObjectId: $davObjectId, ') + ..write('davMemberHref: $davMemberHref, ') + ..write('baselineEtag: $baselineEtag, ') + ..write('baselineRawIcs: $baselineRawIcs, ') + ..write('mutationPatchJson: $mutationPatchJson, ') + ..write('mutationPatchSchemaVersion: $mutationPatchSchemaVersion, ') + ..write('targetComponentKey: $targetComponentKey, ') + ..write('mutationScope: $mutationScope, ') + ..write('destinationCollectionId: $destinationCollectionId, ') + ..write('destinationCollectionHref: $destinationCollectionHref, ') + ..write('destinationMemberHref: $destinationMemberHref, ') + ..write('conflictState: $conflictState, ') + ..write('conflictSnapshotId: $conflictSnapshotId, ') + ..write('retryClassification: $retryClassification, ') + ..write('localTempId: $localTempId, ') + ..write('dependsOnOpId: $dependsOnOpId, ') + ..write('requestJson: $requestJson, ') + ..write('baselineUpdatedUtc: $baselineUpdatedUtc, ') + ..write('baselineRawJson: $baselineRawJson, ') + ..write('attemptCount: $attemptCount, ') + ..write('nextAttemptAtUtc: $nextAttemptAtUtc, ') + ..write('lastErrorCode: $lastErrorCode, ') + ..write('lastErrorMessage: $lastErrorMessage, ') + ..write('state: $state, ') + ..write('lastError: $lastError, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + accountId, + provider, + entityType, + operation, + operationType, + taskListId, + taskId, + calendarSourceId, + providerCalendarId, + eventId, + davCollectionId, + davCollectionHref, + davObjectId, + davMemberHref, + baselineEtag, + baselineRawIcs, + mutationPatchJson, + mutationPatchSchemaVersion, + targetComponentKey, + mutationScope, + destinationCollectionId, + destinationCollectionHref, + destinationMemberHref, + conflictState, + conflictSnapshotId, + retryClassification, + localTempId, + dependsOnOpId, + requestJson, + baselineUpdatedUtc, + baselineRawJson, + attemptCount, + nextAttemptAtUtc, + lastErrorCode, + lastErrorMessage, + state, + lastError, + createdAtUtc, + updatedAtUtc, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PendingOp && + other.id == this.id && + other.accountId == this.accountId && + other.provider == this.provider && + other.entityType == this.entityType && + other.operation == this.operation && + other.operationType == this.operationType && + other.taskListId == this.taskListId && + other.taskId == this.taskId && + other.calendarSourceId == this.calendarSourceId && + other.providerCalendarId == this.providerCalendarId && + other.eventId == this.eventId && + other.davCollectionId == this.davCollectionId && + other.davCollectionHref == this.davCollectionHref && + other.davObjectId == this.davObjectId && + other.davMemberHref == this.davMemberHref && + other.baselineEtag == this.baselineEtag && + other.baselineRawIcs == this.baselineRawIcs && + other.mutationPatchJson == this.mutationPatchJson && + other.mutationPatchSchemaVersion == this.mutationPatchSchemaVersion && + other.targetComponentKey == this.targetComponentKey && + other.mutationScope == this.mutationScope && + other.destinationCollectionId == this.destinationCollectionId && + other.destinationCollectionHref == this.destinationCollectionHref && + other.destinationMemberHref == this.destinationMemberHref && + other.conflictState == this.conflictState && + other.conflictSnapshotId == this.conflictSnapshotId && + other.retryClassification == this.retryClassification && + other.localTempId == this.localTempId && + other.dependsOnOpId == this.dependsOnOpId && + other.requestJson == this.requestJson && + other.baselineUpdatedUtc == this.baselineUpdatedUtc && + other.baselineRawJson == this.baselineRawJson && + other.attemptCount == this.attemptCount && + other.nextAttemptAtUtc == this.nextAttemptAtUtc && + other.lastErrorCode == this.lastErrorCode && + other.lastErrorMessage == this.lastErrorMessage && + other.state == this.state && + other.lastError == this.lastError && + other.createdAtUtc == this.createdAtUtc && + other.updatedAtUtc == this.updatedAtUtc); +} + +class PendingOpsCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value provider; + final Value entityType; + final Value operation; + final Value operationType; + final Value taskListId; + final Value taskId; + final Value calendarSourceId; + final Value providerCalendarId; + final Value eventId; + final Value davCollectionId; + final Value davCollectionHref; + final Value davObjectId; + final Value davMemberHref; + final Value baselineEtag; + final Value baselineRawIcs; + final Value mutationPatchJson; + final Value mutationPatchSchemaVersion; + final Value targetComponentKey; + final Value mutationScope; + final Value destinationCollectionId; + final Value destinationCollectionHref; + final Value destinationMemberHref; + final Value conflictState; + final Value conflictSnapshotId; + final Value retryClassification; + final Value localTempId; + final Value dependsOnOpId; + final Value requestJson; + final Value baselineUpdatedUtc; + final Value baselineRawJson; + final Value attemptCount; + final Value nextAttemptAtUtc; + final Value lastErrorCode; + final Value lastErrorMessage; + final Value state; + final Value lastError; + final Value createdAtUtc; + final Value updatedAtUtc; + final Value rowid; + const PendingOpsCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.provider = const Value.absent(), + this.entityType = const Value.absent(), + this.operation = const Value.absent(), + this.operationType = const Value.absent(), + this.taskListId = const Value.absent(), + this.taskId = const Value.absent(), + this.calendarSourceId = const Value.absent(), + this.providerCalendarId = const Value.absent(), + this.eventId = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.davCollectionHref = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davMemberHref = const Value.absent(), + this.baselineEtag = const Value.absent(), + this.baselineRawIcs = const Value.absent(), + this.mutationPatchJson = const Value.absent(), + this.mutationPatchSchemaVersion = const Value.absent(), + this.targetComponentKey = const Value.absent(), + this.mutationScope = const Value.absent(), + this.destinationCollectionId = const Value.absent(), + this.destinationCollectionHref = const Value.absent(), + this.destinationMemberHref = const Value.absent(), + this.conflictState = const Value.absent(), + this.conflictSnapshotId = const Value.absent(), + this.retryClassification = const Value.absent(), + this.localTempId = const Value.absent(), + this.dependsOnOpId = const Value.absent(), + this.requestJson = const Value.absent(), + this.baselineUpdatedUtc = const Value.absent(), + this.baselineRawJson = const Value.absent(), + this.attemptCount = const Value.absent(), + this.nextAttemptAtUtc = const Value.absent(), + this.lastErrorCode = const Value.absent(), + this.lastErrorMessage = const Value.absent(), + this.state = const Value.absent(), + this.lastError = const Value.absent(), + this.createdAtUtc = const Value.absent(), + this.updatedAtUtc = const Value.absent(), + this.rowid = const Value.absent(), + }); + PendingOpsCompanion.insert({ + required String id, + required String accountId, + this.provider = const Value.absent(), + required String entityType, + required String operation, + this.operationType = const Value.absent(), + this.taskListId = const Value.absent(), + this.taskId = const Value.absent(), + this.calendarSourceId = const Value.absent(), + this.providerCalendarId = const Value.absent(), + this.eventId = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.davCollectionHref = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davMemberHref = const Value.absent(), + this.baselineEtag = const Value.absent(), + this.baselineRawIcs = const Value.absent(), + this.mutationPatchJson = const Value.absent(), + this.mutationPatchSchemaVersion = const Value.absent(), + this.targetComponentKey = const Value.absent(), + this.mutationScope = const Value.absent(), + this.destinationCollectionId = const Value.absent(), + this.destinationCollectionHref = const Value.absent(), + this.destinationMemberHref = const Value.absent(), + this.conflictState = const Value.absent(), + this.conflictSnapshotId = const Value.absent(), + this.retryClassification = const Value.absent(), + this.localTempId = const Value.absent(), + this.dependsOnOpId = const Value.absent(), + required String requestJson, + this.baselineUpdatedUtc = const Value.absent(), + this.baselineRawJson = const Value.absent(), + this.attemptCount = const Value.absent(), + this.nextAttemptAtUtc = const Value.absent(), + this.lastErrorCode = const Value.absent(), + this.lastErrorMessage = const Value.absent(), + this.state = const Value.absent(), + this.lastError = const Value.absent(), + required String createdAtUtc, + required String updatedAtUtc, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + entityType = Value(entityType), + operation = Value(operation), + requestJson = Value(requestJson), + createdAtUtc = Value(createdAtUtc), + updatedAtUtc = Value(updatedAtUtc); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? provider, + Expression? entityType, + Expression? operation, + Expression? operationType, + Expression? taskListId, + Expression? taskId, + Expression? calendarSourceId, + Expression? providerCalendarId, + Expression? eventId, + Expression? davCollectionId, + Expression? davCollectionHref, + Expression? davObjectId, + Expression? davMemberHref, + Expression? baselineEtag, + Expression? baselineRawIcs, + Expression? mutationPatchJson, + Expression? mutationPatchSchemaVersion, + Expression? targetComponentKey, + Expression? mutationScope, + Expression? destinationCollectionId, + Expression? destinationCollectionHref, + Expression? destinationMemberHref, + Expression? conflictState, + Expression? conflictSnapshotId, + Expression? retryClassification, + Expression? localTempId, + Expression? dependsOnOpId, + Expression? requestJson, + Expression? baselineUpdatedUtc, + Expression? baselineRawJson, + Expression? attemptCount, + Expression? nextAttemptAtUtc, + Expression? lastErrorCode, + Expression? lastErrorMessage, + Expression? state, + Expression? lastError, + Expression? createdAtUtc, + Expression? updatedAtUtc, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (provider != null) 'provider': provider, + if (entityType != null) 'entity_type': entityType, + if (operation != null) 'operation': operation, + if (operationType != null) 'operation_type': operationType, + if (taskListId != null) 'task_list_id': taskListId, + if (taskId != null) 'task_id': taskId, + if (calendarSourceId != null) 'calendar_source_id': calendarSourceId, + if (providerCalendarId != null) + 'provider_calendar_id': providerCalendarId, + if (eventId != null) 'event_id': eventId, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (davCollectionHref != null) 'dav_collection_href': davCollectionHref, + if (davObjectId != null) 'dav_object_id': davObjectId, + if (davMemberHref != null) 'dav_member_href': davMemberHref, + if (baselineEtag != null) 'baseline_etag': baselineEtag, + if (baselineRawIcs != null) 'baseline_raw_ics': baselineRawIcs, + if (mutationPatchJson != null) 'mutation_patch_json': mutationPatchJson, + if (mutationPatchSchemaVersion != null) + 'mutation_patch_schema_version': mutationPatchSchemaVersion, + if (targetComponentKey != null) + 'target_component_key': targetComponentKey, + if (mutationScope != null) 'mutation_scope': mutationScope, + if (destinationCollectionId != null) + 'destination_collection_id': destinationCollectionId, + if (destinationCollectionHref != null) + 'destination_collection_href': destinationCollectionHref, + if (destinationMemberHref != null) + 'destination_member_href': destinationMemberHref, + if (conflictState != null) 'conflict_state': conflictState, + if (conflictSnapshotId != null) + 'conflict_snapshot_id': conflictSnapshotId, + if (retryClassification != null) + 'retry_classification': retryClassification, + if (localTempId != null) 'local_temp_id': localTempId, + if (dependsOnOpId != null) 'depends_on_op_id': dependsOnOpId, + if (requestJson != null) 'request_json': requestJson, + if (baselineUpdatedUtc != null) + 'baseline_updated_utc': baselineUpdatedUtc, + if (baselineRawJson != null) 'baseline_raw_json': baselineRawJson, + if (attemptCount != null) 'attempt_count': attemptCount, + if (nextAttemptAtUtc != null) 'next_attempt_at_utc': nextAttemptAtUtc, + if (lastErrorCode != null) 'last_error_code': lastErrorCode, + if (lastErrorMessage != null) 'last_error_message': lastErrorMessage, + if (state != null) 'state': state, + if (lastError != null) 'last_error': lastError, + if (createdAtUtc != null) 'created_at_utc': createdAtUtc, + if (updatedAtUtc != null) 'updated_at_utc': updatedAtUtc, + if (rowid != null) 'rowid': rowid, + }); + } + + PendingOpsCompanion copyWith({ + Value? id, + Value? accountId, + Value? provider, + Value? entityType, + Value? operation, + Value? operationType, + Value? taskListId, + Value? taskId, + Value? calendarSourceId, + Value? providerCalendarId, + Value? eventId, + Value? davCollectionId, + Value? davCollectionHref, + Value? davObjectId, + Value? davMemberHref, + Value? baselineEtag, + Value? baselineRawIcs, + Value? mutationPatchJson, + Value? mutationPatchSchemaVersion, + Value? targetComponentKey, + Value? mutationScope, + Value? destinationCollectionId, + Value? destinationCollectionHref, + Value? destinationMemberHref, + Value? conflictState, + Value? conflictSnapshotId, + Value? retryClassification, + Value? localTempId, + Value? dependsOnOpId, + Value? requestJson, + Value? baselineUpdatedUtc, + Value? baselineRawJson, + Value? attemptCount, + Value? nextAttemptAtUtc, + Value? lastErrorCode, + Value? lastErrorMessage, + Value? state, + Value? lastError, + Value? createdAtUtc, + Value? updatedAtUtc, + Value? rowid, + }) { + return PendingOpsCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider ?? this.provider, + entityType: entityType ?? this.entityType, + operation: operation ?? this.operation, + operationType: operationType ?? this.operationType, + taskListId: taskListId ?? this.taskListId, + taskId: taskId ?? this.taskId, + calendarSourceId: calendarSourceId ?? this.calendarSourceId, + providerCalendarId: providerCalendarId ?? this.providerCalendarId, + eventId: eventId ?? this.eventId, + davCollectionId: davCollectionId ?? this.davCollectionId, + davCollectionHref: davCollectionHref ?? this.davCollectionHref, + davObjectId: davObjectId ?? this.davObjectId, + davMemberHref: davMemberHref ?? this.davMemberHref, + baselineEtag: baselineEtag ?? this.baselineEtag, + baselineRawIcs: baselineRawIcs ?? this.baselineRawIcs, + mutationPatchJson: mutationPatchJson ?? this.mutationPatchJson, + mutationPatchSchemaVersion: + mutationPatchSchemaVersion ?? this.mutationPatchSchemaVersion, + targetComponentKey: targetComponentKey ?? this.targetComponentKey, + mutationScope: mutationScope ?? this.mutationScope, + destinationCollectionId: + destinationCollectionId ?? this.destinationCollectionId, + destinationCollectionHref: + destinationCollectionHref ?? this.destinationCollectionHref, + destinationMemberHref: + destinationMemberHref ?? this.destinationMemberHref, + conflictState: conflictState ?? this.conflictState, + conflictSnapshotId: conflictSnapshotId ?? this.conflictSnapshotId, + retryClassification: retryClassification ?? this.retryClassification, + localTempId: localTempId ?? this.localTempId, + dependsOnOpId: dependsOnOpId ?? this.dependsOnOpId, + requestJson: requestJson ?? this.requestJson, + baselineUpdatedUtc: baselineUpdatedUtc ?? this.baselineUpdatedUtc, + baselineRawJson: baselineRawJson ?? this.baselineRawJson, + attemptCount: attemptCount ?? this.attemptCount, + nextAttemptAtUtc: nextAttemptAtUtc ?? this.nextAttemptAtUtc, + lastErrorCode: lastErrorCode ?? this.lastErrorCode, + lastErrorMessage: lastErrorMessage ?? this.lastErrorMessage, + state: state ?? this.state, + lastError: lastError ?? this.lastError, + createdAtUtc: createdAtUtc ?? this.createdAtUtc, + updatedAtUtc: updatedAtUtc ?? this.updatedAtUtc, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (entityType.present) { + map['entity_type'] = Variable(entityType.value); + } + if (operation.present) { + map['operation'] = Variable(operation.value); + } + if (operationType.present) { + map['operation_type'] = Variable(operationType.value); + } + if (taskListId.present) { + map['task_list_id'] = Variable(taskListId.value); + } + if (taskId.present) { + map['task_id'] = Variable(taskId.value); + } + if (calendarSourceId.present) { + map['calendar_source_id'] = Variable(calendarSourceId.value); + } + if (providerCalendarId.present) { + map['provider_calendar_id'] = Variable(providerCalendarId.value); + } + if (eventId.present) { + map['event_id'] = Variable(eventId.value); + } + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); + } + if (davCollectionHref.present) { + map['dav_collection_href'] = Variable(davCollectionHref.value); + } + if (davObjectId.present) { + map['dav_object_id'] = Variable(davObjectId.value); + } + if (davMemberHref.present) { + map['dav_member_href'] = Variable(davMemberHref.value); + } + if (baselineEtag.present) { + map['baseline_etag'] = Variable(baselineEtag.value); + } + if (baselineRawIcs.present) { + map['baseline_raw_ics'] = Variable(baselineRawIcs.value); + } + if (mutationPatchJson.present) { + map['mutation_patch_json'] = Variable(mutationPatchJson.value); + } + if (mutationPatchSchemaVersion.present) { + map['mutation_patch_schema_version'] = Variable( + mutationPatchSchemaVersion.value, + ); + } + if (targetComponentKey.present) { + map['target_component_key'] = Variable(targetComponentKey.value); + } + if (mutationScope.present) { + map['mutation_scope'] = Variable(mutationScope.value); + } + if (destinationCollectionId.present) { + map['destination_collection_id'] = Variable( + destinationCollectionId.value, + ); + } + if (destinationCollectionHref.present) { + map['destination_collection_href'] = Variable( + destinationCollectionHref.value, + ); + } + if (destinationMemberHref.present) { + map['destination_member_href'] = Variable( + destinationMemberHref.value, + ); + } + if (conflictState.present) { + map['conflict_state'] = Variable(conflictState.value); + } + if (conflictSnapshotId.present) { + map['conflict_snapshot_id'] = Variable(conflictSnapshotId.value); + } + if (retryClassification.present) { + map['retry_classification'] = Variable(retryClassification.value); + } + if (localTempId.present) { + map['local_temp_id'] = Variable(localTempId.value); + } + if (dependsOnOpId.present) { + map['depends_on_op_id'] = Variable(dependsOnOpId.value); + } + if (requestJson.present) { + map['request_json'] = Variable(requestJson.value); + } + if (baselineUpdatedUtc.present) { + map['baseline_updated_utc'] = Variable(baselineUpdatedUtc.value); + } + if (baselineRawJson.present) { + map['baseline_raw_json'] = Variable(baselineRawJson.value); + } + if (attemptCount.present) { + map['attempt_count'] = Variable(attemptCount.value); + } + if (nextAttemptAtUtc.present) { + map['next_attempt_at_utc'] = Variable(nextAttemptAtUtc.value); + } + if (lastErrorCode.present) { + map['last_error_code'] = Variable(lastErrorCode.value); + } + if (lastErrorMessage.present) { + map['last_error_message'] = Variable(lastErrorMessage.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (lastError.present) { + map['last_error'] = Variable(lastError.value); + } + if (createdAtUtc.present) { + map['created_at_utc'] = Variable(createdAtUtc.value); + } + if (updatedAtUtc.present) { + map['updated_at_utc'] = Variable(updatedAtUtc.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PendingOpsCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('entityType: $entityType, ') + ..write('operation: $operation, ') + ..write('operationType: $operationType, ') + ..write('taskListId: $taskListId, ') + ..write('taskId: $taskId, ') + ..write('calendarSourceId: $calendarSourceId, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('eventId: $eventId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davCollectionHref: $davCollectionHref, ') + ..write('davObjectId: $davObjectId, ') + ..write('davMemberHref: $davMemberHref, ') + ..write('baselineEtag: $baselineEtag, ') + ..write('baselineRawIcs: $baselineRawIcs, ') + ..write('mutationPatchJson: $mutationPatchJson, ') + ..write('mutationPatchSchemaVersion: $mutationPatchSchemaVersion, ') + ..write('targetComponentKey: $targetComponentKey, ') + ..write('mutationScope: $mutationScope, ') + ..write('destinationCollectionId: $destinationCollectionId, ') + ..write('destinationCollectionHref: $destinationCollectionHref, ') + ..write('destinationMemberHref: $destinationMemberHref, ') + ..write('conflictState: $conflictState, ') + ..write('conflictSnapshotId: $conflictSnapshotId, ') + ..write('retryClassification: $retryClassification, ') + ..write('localTempId: $localTempId, ') + ..write('dependsOnOpId: $dependsOnOpId, ') + ..write('requestJson: $requestJson, ') + ..write('baselineUpdatedUtc: $baselineUpdatedUtc, ') + ..write('baselineRawJson: $baselineRawJson, ') + ..write('attemptCount: $attemptCount, ') + ..write('nextAttemptAtUtc: $nextAttemptAtUtc, ') + ..write('lastErrorCode: $lastErrorCode, ') + ..write('lastErrorMessage: $lastErrorMessage, ') + ..write('state: $state, ') + ..write('lastError: $lastError, ') + ..write('createdAtUtc: $createdAtUtc, ') + ..write('updatedAtUtc: $updatedAtUtc, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SyncRunsTable extends SyncRuns with TableInfo<$SyncRunsTable, SyncRun> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SyncRunsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _modeMeta = const VerificationMeta('mode'); + @override + late final GeneratedColumn mode = GeneratedColumn( + 'mode', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _startedAtUtcMeta = const VerificationMeta( + 'startedAtUtc', + ); + @override + late final GeneratedColumn startedAtUtc = GeneratedColumn( + 'started_at_utc', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _finishedAtUtcMeta = const VerificationMeta( + 'finishedAtUtc', + ); + @override + late final GeneratedColumn finishedAtUtc = GeneratedColumn( + 'finished_at_utc', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _taskListsSeenMeta = const VerificationMeta( + 'taskListsSeen', + ); + @override + late final GeneratedColumn taskListsSeen = GeneratedColumn( + 'task_lists_seen', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _tasksSeenMeta = const VerificationMeta( + 'tasksSeen', + ); + @override + late final GeneratedColumn tasksSeen = GeneratedColumn( + 'tasks_seen', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _pendingOpsAppliedMeta = const VerificationMeta( + 'pendingOpsApplied', + ); + @override + late final GeneratedColumn pendingOpsApplied = GeneratedColumn( + 'pending_ops_applied', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _errorCodeMeta = const VerificationMeta( + 'errorCode', + ); + @override + late final GeneratedColumn errorCode = GeneratedColumn( + 'error_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _errorMessageMeta = const VerificationMeta( + 'errorMessage', + ); + @override + late final GeneratedColumn errorMessage = GeneratedColumn( + 'error_message', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + accountId, + provider, + mode, + startedAtUtc, + finishedAtUtc, + status, + taskListsSeen, + tasksSeen, + pendingOpsApplied, + errorCode, + errorMessage, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sync_runs'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } + if (data.containsKey('mode')) { + context.handle( + _modeMeta, + mode.isAcceptableOrUnknown(data['mode']!, _modeMeta), + ); + } else if (isInserting) { + context.missing(_modeMeta); + } + if (data.containsKey('started_at_utc')) { + context.handle( + _startedAtUtcMeta, + startedAtUtc.isAcceptableOrUnknown( + data['started_at_utc']!, + _startedAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_startedAtUtcMeta); + } + if (data.containsKey('finished_at_utc')) { + context.handle( + _finishedAtUtcMeta, + finishedAtUtc.isAcceptableOrUnknown( + data['finished_at_utc']!, + _finishedAtUtcMeta, + ), + ); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } else if (isInserting) { + context.missing(_statusMeta); + } + if (data.containsKey('task_lists_seen')) { + context.handle( + _taskListsSeenMeta, + taskListsSeen.isAcceptableOrUnknown( + data['task_lists_seen']!, + _taskListsSeenMeta, + ), + ); + } + if (data.containsKey('tasks_seen')) { + context.handle( + _tasksSeenMeta, + tasksSeen.isAcceptableOrUnknown(data['tasks_seen']!, _tasksSeenMeta), + ); + } + if (data.containsKey('pending_ops_applied')) { + context.handle( + _pendingOpsAppliedMeta, + pendingOpsApplied.isAcceptableOrUnknown( + data['pending_ops_applied']!, + _pendingOpsAppliedMeta, + ), + ); + } + if (data.containsKey('error_code')) { + context.handle( + _errorCodeMeta, + errorCode.isAcceptableOrUnknown(data['error_code']!, _errorCodeMeta), + ); + } + if (data.containsKey('error_message')) { + context.handle( + _errorMessageMeta, + errorMessage.isAcceptableOrUnknown( + data['error_message']!, + _errorMessageMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SyncRun map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SyncRun( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + ), + mode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}mode'], + )!, + startedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}started_at_utc'], + )!, + finishedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}finished_at_utc'], + ), + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + )!, + taskListsSeen: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}task_lists_seen'], + )!, + tasksSeen: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}tasks_seen'], + )!, + pendingOpsApplied: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}pending_ops_applied'], + )!, + errorCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_code'], + ), + errorMessage: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}error_message'], + ), + ); + } + + @override + $SyncRunsTable createAlias(String alias) { + return $SyncRunsTable(attachedDatabase, alias); + } +} + +class SyncRun extends DataClass implements Insertable { + final String id; + final String accountId; + final String? provider; + final String mode; + final String startedAtUtc; + final String? finishedAtUtc; + final String status; + final int taskListsSeen; + final int tasksSeen; + final int pendingOpsApplied; + final String? errorCode; + final String? errorMessage; + const SyncRun({ + required this.id, + required this.accountId, + this.provider, + required this.mode, + required this.startedAtUtc, + this.finishedAtUtc, + required this.status, + required this.taskListsSeen, + required this.tasksSeen, + required this.pendingOpsApplied, + this.errorCode, + this.errorMessage, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + if (!nullToAbsent || provider != null) { + map['provider'] = Variable(provider); + } + map['mode'] = Variable(mode); + map['started_at_utc'] = Variable(startedAtUtc); + if (!nullToAbsent || finishedAtUtc != null) { + map['finished_at_utc'] = Variable(finishedAtUtc); + } + map['status'] = Variable(status); + map['task_lists_seen'] = Variable(taskListsSeen); + map['tasks_seen'] = Variable(tasksSeen); + map['pending_ops_applied'] = Variable(pendingOpsApplied); + if (!nullToAbsent || errorCode != null) { + map['error_code'] = Variable(errorCode); + } + if (!nullToAbsent || errorMessage != null) { + map['error_message'] = Variable(errorMessage); + } + return map; + } + + SyncRunsCompanion toCompanion(bool nullToAbsent) { + return SyncRunsCompanion( + id: Value(id), + accountId: Value(accountId), + provider: provider == null && nullToAbsent + ? const Value.absent() + : Value(provider), + mode: Value(mode), + startedAtUtc: Value(startedAtUtc), + finishedAtUtc: finishedAtUtc == null && nullToAbsent + ? const Value.absent() + : Value(finishedAtUtc), + status: Value(status), + taskListsSeen: Value(taskListsSeen), + tasksSeen: Value(tasksSeen), + pendingOpsApplied: Value(pendingOpsApplied), + errorCode: errorCode == null && nullToAbsent + ? const Value.absent() + : Value(errorCode), + errorMessage: errorMessage == null && nullToAbsent + ? const Value.absent() + : Value(errorMessage), + ); + } + + factory SyncRun.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SyncRun( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + provider: serializer.fromJson(json['provider']), + mode: serializer.fromJson(json['mode']), + startedAtUtc: serializer.fromJson(json['startedAtUtc']), + finishedAtUtc: serializer.fromJson(json['finishedAtUtc']), + status: serializer.fromJson(json['status']), + taskListsSeen: serializer.fromJson(json['taskListsSeen']), + tasksSeen: serializer.fromJson(json['tasksSeen']), + pendingOpsApplied: serializer.fromJson(json['pendingOpsApplied']), + errorCode: serializer.fromJson(json['errorCode']), + errorMessage: serializer.fromJson(json['errorMessage']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'provider': serializer.toJson(provider), + 'mode': serializer.toJson(mode), + 'startedAtUtc': serializer.toJson(startedAtUtc), + 'finishedAtUtc': serializer.toJson(finishedAtUtc), + 'status': serializer.toJson(status), + 'taskListsSeen': serializer.toJson(taskListsSeen), + 'tasksSeen': serializer.toJson(tasksSeen), + 'pendingOpsApplied': serializer.toJson(pendingOpsApplied), + 'errorCode': serializer.toJson(errorCode), + 'errorMessage': serializer.toJson(errorMessage), + }; + } + + SyncRun copyWith({ + String? id, + String? accountId, + Value provider = const Value.absent(), + String? mode, + String? startedAtUtc, + Value finishedAtUtc = const Value.absent(), + String? status, + int? taskListsSeen, + int? tasksSeen, + int? pendingOpsApplied, + Value errorCode = const Value.absent(), + Value errorMessage = const Value.absent(), + }) => SyncRun( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider.present ? provider.value : this.provider, + mode: mode ?? this.mode, + startedAtUtc: startedAtUtc ?? this.startedAtUtc, + finishedAtUtc: finishedAtUtc.present + ? finishedAtUtc.value + : this.finishedAtUtc, + status: status ?? this.status, + taskListsSeen: taskListsSeen ?? this.taskListsSeen, + tasksSeen: tasksSeen ?? this.tasksSeen, + pendingOpsApplied: pendingOpsApplied ?? this.pendingOpsApplied, + errorCode: errorCode.present ? errorCode.value : this.errorCode, + errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage, + ); + SyncRun copyWithCompanion(SyncRunsCompanion data) { + return SyncRun( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + provider: data.provider.present ? data.provider.value : this.provider, + mode: data.mode.present ? data.mode.value : this.mode, + startedAtUtc: data.startedAtUtc.present + ? data.startedAtUtc.value + : this.startedAtUtc, + finishedAtUtc: data.finishedAtUtc.present + ? data.finishedAtUtc.value + : this.finishedAtUtc, + status: data.status.present ? data.status.value : this.status, + taskListsSeen: data.taskListsSeen.present + ? data.taskListsSeen.value + : this.taskListsSeen, + tasksSeen: data.tasksSeen.present ? data.tasksSeen.value : this.tasksSeen, + pendingOpsApplied: data.pendingOpsApplied.present + ? data.pendingOpsApplied.value + : this.pendingOpsApplied, + errorCode: data.errorCode.present ? data.errorCode.value : this.errorCode, + errorMessage: data.errorMessage.present + ? data.errorMessage.value + : this.errorMessage, + ); + } + + @override + String toString() { + return (StringBuffer('SyncRun(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('mode: $mode, ') + ..write('startedAtUtc: $startedAtUtc, ') + ..write('finishedAtUtc: $finishedAtUtc, ') + ..write('status: $status, ') + ..write('taskListsSeen: $taskListsSeen, ') + ..write('tasksSeen: $tasksSeen, ') + ..write('pendingOpsApplied: $pendingOpsApplied, ') + ..write('errorCode: $errorCode, ') + ..write('errorMessage: $errorMessage') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + accountId, + provider, + mode, + startedAtUtc, + finishedAtUtc, + status, + taskListsSeen, + tasksSeen, + pendingOpsApplied, + errorCode, + errorMessage, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SyncRun && + other.id == this.id && + other.accountId == this.accountId && + other.provider == this.provider && + other.mode == this.mode && + other.startedAtUtc == this.startedAtUtc && + other.finishedAtUtc == this.finishedAtUtc && + other.status == this.status && + other.taskListsSeen == this.taskListsSeen && + other.tasksSeen == this.tasksSeen && + other.pendingOpsApplied == this.pendingOpsApplied && + other.errorCode == this.errorCode && + other.errorMessage == this.errorMessage); +} + +class SyncRunsCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value provider; + final Value mode; + final Value startedAtUtc; + final Value finishedAtUtc; + final Value status; + final Value taskListsSeen; + final Value tasksSeen; + final Value pendingOpsApplied; + final Value errorCode; + final Value errorMessage; + final Value rowid; + const SyncRunsCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.provider = const Value.absent(), + this.mode = const Value.absent(), + this.startedAtUtc = const Value.absent(), + this.finishedAtUtc = const Value.absent(), + this.status = const Value.absent(), + this.taskListsSeen = const Value.absent(), + this.tasksSeen = const Value.absent(), + this.pendingOpsApplied = const Value.absent(), + this.errorCode = const Value.absent(), + this.errorMessage = const Value.absent(), + this.rowid = const Value.absent(), + }); + SyncRunsCompanion.insert({ + required String id, + required String accountId, + this.provider = const Value.absent(), + required String mode, + required String startedAtUtc, + this.finishedAtUtc = const Value.absent(), + required String status, + this.taskListsSeen = const Value.absent(), + this.tasksSeen = const Value.absent(), + this.pendingOpsApplied = const Value.absent(), + this.errorCode = const Value.absent(), + this.errorMessage = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + mode = Value(mode), + startedAtUtc = Value(startedAtUtc), + status = Value(status); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? provider, + Expression? mode, + Expression? startedAtUtc, + Expression? finishedAtUtc, + Expression? status, + Expression? taskListsSeen, + Expression? tasksSeen, + Expression? pendingOpsApplied, + Expression? errorCode, + Expression? errorMessage, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (provider != null) 'provider': provider, + if (mode != null) 'mode': mode, + if (startedAtUtc != null) 'started_at_utc': startedAtUtc, + if (finishedAtUtc != null) 'finished_at_utc': finishedAtUtc, + if (status != null) 'status': status, + if (taskListsSeen != null) 'task_lists_seen': taskListsSeen, + if (tasksSeen != null) 'tasks_seen': tasksSeen, + if (pendingOpsApplied != null) 'pending_ops_applied': pendingOpsApplied, + if (errorCode != null) 'error_code': errorCode, + if (errorMessage != null) 'error_message': errorMessage, + if (rowid != null) 'rowid': rowid, + }); + } + + SyncRunsCompanion copyWith({ + Value? id, + Value? accountId, + Value? provider, + Value? mode, + Value? startedAtUtc, + Value? finishedAtUtc, + Value? status, + Value? taskListsSeen, + Value? tasksSeen, + Value? pendingOpsApplied, + Value? errorCode, + Value? errorMessage, + Value? rowid, + }) { + return SyncRunsCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider ?? this.provider, + mode: mode ?? this.mode, + startedAtUtc: startedAtUtc ?? this.startedAtUtc, + finishedAtUtc: finishedAtUtc ?? this.finishedAtUtc, + status: status ?? this.status, + taskListsSeen: taskListsSeen ?? this.taskListsSeen, + tasksSeen: tasksSeen ?? this.tasksSeen, + pendingOpsApplied: pendingOpsApplied ?? this.pendingOpsApplied, + errorCode: errorCode ?? this.errorCode, + errorMessage: errorMessage ?? this.errorMessage, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (mode.present) { + map['mode'] = Variable(mode.value); + } + if (startedAtUtc.present) { + map['started_at_utc'] = Variable(startedAtUtc.value); + } + if (finishedAtUtc.present) { + map['finished_at_utc'] = Variable(finishedAtUtc.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (taskListsSeen.present) { + map['task_lists_seen'] = Variable(taskListsSeen.value); + } + if (tasksSeen.present) { + map['tasks_seen'] = Variable(tasksSeen.value); + } + if (pendingOpsApplied.present) { + map['pending_ops_applied'] = Variable(pendingOpsApplied.value); + } + if (errorCode.present) { + map['error_code'] = Variable(errorCode.value); + } + if (errorMessage.present) { + map['error_message'] = Variable(errorMessage.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SyncRunsCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('mode: $mode, ') + ..write('startedAtUtc: $startedAtUtc, ') + ..write('finishedAtUtc: $finishedAtUtc, ') + ..write('status: $status, ') + ..write('taskListsSeen: $taskListsSeen, ') + ..write('tasksSeen: $tasksSeen, ') + ..write('pendingOpsApplied: $pendingOpsApplied, ') + ..write('errorCode: $errorCode, ') + ..write('errorMessage: $errorMessage, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CalendarSourcesTable extends CalendarSources + with TableInfo<$CalendarSourcesTable, CalendarSource> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CalendarSourcesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _providerCalendarIdMeta = + const VerificationMeta('providerCalendarId'); + @override + late final GeneratedColumn providerCalendarId = + GeneratedColumn( + 'provider_calendar_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', + ); + @override + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _summaryMeta = const VerificationMeta( + 'summary', + ); + @override + late final GeneratedColumn summary = GeneratedColumn( + 'summary', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', + ); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _primaryCalendarMeta = const VerificationMeta( + 'primaryCalendar', + ); + @override + late final GeneratedColumn primaryCalendar = GeneratedColumn( + 'primary_calendar', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("primary_calendar" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _selectedMeta = const VerificationMeta( + 'selected', + ); + @override + late final GeneratedColumn selected = GeneratedColumn( + 'selected', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("selected" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _hiddenMeta = const VerificationMeta('hidden'); + @override + late final GeneratedColumn hidden = GeneratedColumn( + 'hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("hidden" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _readOnlyMeta = const VerificationMeta( + 'readOnly', + ); + @override + late final GeneratedColumn readOnly = GeneratedColumn( + 'read_only', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("read_only" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _backgroundColorMeta = const VerificationMeta( + 'backgroundColor', + ); + @override + late final GeneratedColumn backgroundColor = GeneratedColumn( + 'background_color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _foregroundColorMeta = const VerificationMeta( + 'foregroundColor', + ); + @override + late final GeneratedColumn foregroundColor = GeneratedColumn( + 'foreground_color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _colorIdMeta = const VerificationMeta( + 'colorId', + ); + @override + late final GeneratedColumn colorId = GeneratedColumn( + 'color_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _timeZoneMeta = const VerificationMeta( + 'timeZone', + ); + @override + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _accessRoleMeta = const VerificationMeta( + 'accessRole', + ); + @override + late final GeneratedColumn accessRole = GeneratedColumn( + 'access_role', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isDeletedMeta = const VerificationMeta( + 'isDeleted', + ); + @override + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_deleted" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', + ); + @override + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( + 'createdAtLocal', + ); + @override + late final GeneratedColumn createdAtLocal = GeneratedColumn( + 'created_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( + 'updatedAtLocal', + ); + @override + late final GeneratedColumn updatedAtLocal = GeneratedColumn( + 'updated_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + accountId, + provider, + providerCalendarId, + davCollectionId, + summary, + description, + primaryCalendar, + selected, + hidden, + readOnly, + backgroundColor, + foregroundColor, + colorId, + timeZone, + accessRole, + isDeleted, + rawJson, + createdAtLocal, + updatedAtLocal, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'calendar_sources'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('provider_calendar_id')) { + context.handle( + _providerCalendarIdMeta, + providerCalendarId.isAcceptableOrUnknown( + data['provider_calendar_id']!, + _providerCalendarIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_providerCalendarIdMeta); + } + if (data.containsKey('dav_collection_id')) { + context.handle( + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), + ); + } + if (data.containsKey('summary')) { + context.handle( + _summaryMeta, + summary.isAcceptableOrUnknown(data['summary']!, _summaryMeta), + ); + } else if (isInserting) { + context.missing(_summaryMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), + ); + } + if (data.containsKey('primary_calendar')) { + context.handle( + _primaryCalendarMeta, + primaryCalendar.isAcceptableOrUnknown( + data['primary_calendar']!, + _primaryCalendarMeta, + ), + ); + } + if (data.containsKey('selected')) { + context.handle( + _selectedMeta, + selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta), + ); + } + if (data.containsKey('hidden')) { + context.handle( + _hiddenMeta, + hidden.isAcceptableOrUnknown(data['hidden']!, _hiddenMeta), + ); + } + if (data.containsKey('read_only')) { + context.handle( + _readOnlyMeta, + readOnly.isAcceptableOrUnknown(data['read_only']!, _readOnlyMeta), + ); + } + if (data.containsKey('background_color')) { + context.handle( + _backgroundColorMeta, + backgroundColor.isAcceptableOrUnknown( + data['background_color']!, + _backgroundColorMeta, + ), + ); + } + if (data.containsKey('foreground_color')) { + context.handle( + _foregroundColorMeta, + foregroundColor.isAcceptableOrUnknown( + data['foreground_color']!, + _foregroundColorMeta, + ), + ); + } + if (data.containsKey('color_id')) { + context.handle( + _colorIdMeta, + colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), + ); + } + if (data.containsKey('time_zone')) { + context.handle( + _timeZoneMeta, + timeZone.isAcceptableOrUnknown(data['time_zone']!, _timeZoneMeta), + ); + } + if (data.containsKey('access_role')) { + context.handle( + _accessRoleMeta, + accessRole.isAcceptableOrUnknown(data['access_role']!, _accessRoleMeta), + ); + } + if (data.containsKey('is_deleted')) { + context.handle( + _isDeletedMeta, + isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } + if (data.containsKey('created_at_local')) { + context.handle( + _createdAtLocalMeta, + createdAtLocal.isAcceptableOrUnknown( + data['created_at_local']!, + _createdAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdAtLocalMeta); + } + if (data.containsKey('updated_at_local')) { + context.handle( + _updatedAtLocalMeta, + updatedAtLocal.isAcceptableOrUnknown( + data['updated_at_local']!, + _updatedAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedAtLocalMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CalendarSource map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CalendarSource( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + )!, + providerCalendarId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_calendar_id'], + )!, + davCollectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_collection_id'], + ), + summary: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}summary'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + primaryCalendar: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}primary_calendar'], + )!, + selected: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}selected'], + )!, + hidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}hidden'], + )!, + readOnly: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}read_only'], + )!, + backgroundColor: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}background_color'], + ), + foregroundColor: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}foreground_color'], + ), + colorId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color_id'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + accessRole: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}access_role'], + ), + isDeleted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_deleted'], + )!, + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + ), + createdAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at_local'], + )!, + updatedAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at_local'], + )!, + ); + } + + @override + $CalendarSourcesTable createAlias(String alias) { + return $CalendarSourcesTable(attachedDatabase, alias); + } +} + +class CalendarSource extends DataClass implements Insertable { + final String id; + final String accountId; + final String provider; + final String providerCalendarId; + final String? davCollectionId; + final String summary; + final String? description; + final bool primaryCalendar; + final bool selected; + final bool hidden; + final bool readOnly; + final String? backgroundColor; + final String? foregroundColor; + final String? colorId; + final String? timeZone; + final String? accessRole; + final bool isDeleted; + final String? rawJson; + final int createdAtLocal; + final int updatedAtLocal; + const CalendarSource({ + required this.id, + required this.accountId, + required this.provider, + required this.providerCalendarId, + this.davCollectionId, + required this.summary, + this.description, + required this.primaryCalendar, + required this.selected, + required this.hidden, + required this.readOnly, + this.backgroundColor, + this.foregroundColor, + this.colorId, + this.timeZone, + this.accessRole, + required this.isDeleted, + this.rawJson, + required this.createdAtLocal, + required this.updatedAtLocal, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + map['provider'] = Variable(provider); + map['provider_calendar_id'] = Variable(providerCalendarId); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); + } + map['summary'] = Variable(summary); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + map['primary_calendar'] = Variable(primaryCalendar); + map['selected'] = Variable(selected); + map['hidden'] = Variable(hidden); + map['read_only'] = Variable(readOnly); + if (!nullToAbsent || backgroundColor != null) { + map['background_color'] = Variable(backgroundColor); + } + if (!nullToAbsent || foregroundColor != null) { + map['foreground_color'] = Variable(foregroundColor); + } + if (!nullToAbsent || colorId != null) { + map['color_id'] = Variable(colorId); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || accessRole != null) { + map['access_role'] = Variable(accessRole); + } + map['is_deleted'] = Variable(isDeleted); + if (!nullToAbsent || rawJson != null) { + map['raw_json'] = Variable(rawJson); + } + map['created_at_local'] = Variable(createdAtLocal); + map['updated_at_local'] = Variable(updatedAtLocal); + return map; + } + + CalendarSourcesCompanion toCompanion(bool nullToAbsent) { + return CalendarSourcesCompanion( + id: Value(id), + accountId: Value(accountId), + provider: Value(provider), + providerCalendarId: Value(providerCalendarId), + davCollectionId: davCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionId), + summary: Value(summary), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + primaryCalendar: Value(primaryCalendar), + selected: Value(selected), + hidden: Value(hidden), + readOnly: Value(readOnly), + backgroundColor: backgroundColor == null && nullToAbsent + ? const Value.absent() + : Value(backgroundColor), + foregroundColor: foregroundColor == null && nullToAbsent + ? const Value.absent() + : Value(foregroundColor), + colorId: colorId == null && nullToAbsent + ? const Value.absent() + : Value(colorId), + timeZone: timeZone == null && nullToAbsent + ? const Value.absent() + : Value(timeZone), + accessRole: accessRole == null && nullToAbsent + ? const Value.absent() + : Value(accessRole), + isDeleted: Value(isDeleted), + rawJson: rawJson == null && nullToAbsent + ? const Value.absent() + : Value(rawJson), + createdAtLocal: Value(createdAtLocal), + updatedAtLocal: Value(updatedAtLocal), + ); + } + + factory CalendarSource.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CalendarSource( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + provider: serializer.fromJson(json['provider']), + providerCalendarId: serializer.fromJson( + json['providerCalendarId'], + ), + davCollectionId: serializer.fromJson(json['davCollectionId']), + summary: serializer.fromJson(json['summary']), + description: serializer.fromJson(json['description']), + primaryCalendar: serializer.fromJson(json['primaryCalendar']), + selected: serializer.fromJson(json['selected']), + hidden: serializer.fromJson(json['hidden']), + readOnly: serializer.fromJson(json['readOnly']), + backgroundColor: serializer.fromJson(json['backgroundColor']), + foregroundColor: serializer.fromJson(json['foregroundColor']), + colorId: serializer.fromJson(json['colorId']), + timeZone: serializer.fromJson(json['timeZone']), + accessRole: serializer.fromJson(json['accessRole']), + isDeleted: serializer.fromJson(json['isDeleted']), + rawJson: serializer.fromJson(json['rawJson']), + createdAtLocal: serializer.fromJson(json['createdAtLocal']), + updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'provider': serializer.toJson(provider), + 'providerCalendarId': serializer.toJson(providerCalendarId), + 'davCollectionId': serializer.toJson(davCollectionId), + 'summary': serializer.toJson(summary), + 'description': serializer.toJson(description), + 'primaryCalendar': serializer.toJson(primaryCalendar), + 'selected': serializer.toJson(selected), + 'hidden': serializer.toJson(hidden), + 'readOnly': serializer.toJson(readOnly), + 'backgroundColor': serializer.toJson(backgroundColor), + 'foregroundColor': serializer.toJson(foregroundColor), + 'colorId': serializer.toJson(colorId), + 'timeZone': serializer.toJson(timeZone), + 'accessRole': serializer.toJson(accessRole), + 'isDeleted': serializer.toJson(isDeleted), + 'rawJson': serializer.toJson(rawJson), + 'createdAtLocal': serializer.toJson(createdAtLocal), + 'updatedAtLocal': serializer.toJson(updatedAtLocal), + }; + } + + CalendarSource copyWith({ + String? id, + String? accountId, + String? provider, + String? providerCalendarId, + Value davCollectionId = const Value.absent(), + String? summary, + Value description = const Value.absent(), + bool? primaryCalendar, + bool? selected, + bool? hidden, + bool? readOnly, + Value backgroundColor = const Value.absent(), + Value foregroundColor = const Value.absent(), + Value colorId = const Value.absent(), + Value timeZone = const Value.absent(), + Value accessRole = const Value.absent(), + bool? isDeleted, + Value rawJson = const Value.absent(), + int? createdAtLocal, + int? updatedAtLocal, + }) => CalendarSource( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider ?? this.provider, + providerCalendarId: providerCalendarId ?? this.providerCalendarId, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + summary: summary ?? this.summary, + description: description.present ? description.value : this.description, + primaryCalendar: primaryCalendar ?? this.primaryCalendar, + selected: selected ?? this.selected, + hidden: hidden ?? this.hidden, + readOnly: readOnly ?? this.readOnly, + backgroundColor: backgroundColor.present + ? backgroundColor.value + : this.backgroundColor, + foregroundColor: foregroundColor.present + ? foregroundColor.value + : this.foregroundColor, + colorId: colorId.present ? colorId.value : this.colorId, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + accessRole: accessRole.present ? accessRole.value : this.accessRole, + isDeleted: isDeleted ?? this.isDeleted, + rawJson: rawJson.present ? rawJson.value : this.rawJson, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + ); + CalendarSource copyWithCompanion(CalendarSourcesCompanion data) { + return CalendarSource( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + provider: data.provider.present ? data.provider.value : this.provider, + providerCalendarId: data.providerCalendarId.present + ? data.providerCalendarId.value + : this.providerCalendarId, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + summary: data.summary.present ? data.summary.value : this.summary, + description: data.description.present + ? data.description.value + : this.description, + primaryCalendar: data.primaryCalendar.present + ? data.primaryCalendar.value + : this.primaryCalendar, + selected: data.selected.present ? data.selected.value : this.selected, + hidden: data.hidden.present ? data.hidden.value : this.hidden, + readOnly: data.readOnly.present ? data.readOnly.value : this.readOnly, + backgroundColor: data.backgroundColor.present + ? data.backgroundColor.value + : this.backgroundColor, + foregroundColor: data.foregroundColor.present + ? data.foregroundColor.value + : this.foregroundColor, + colorId: data.colorId.present ? data.colorId.value : this.colorId, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + accessRole: data.accessRole.present + ? data.accessRole.value + : this.accessRole, + isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + createdAtLocal: data.createdAtLocal.present + ? data.createdAtLocal.value + : this.createdAtLocal, + updatedAtLocal: data.updatedAtLocal.present + ? data.updatedAtLocal.value + : this.updatedAtLocal, + ); + } + + @override + String toString() { + return (StringBuffer('CalendarSource(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('summary: $summary, ') + ..write('description: $description, ') + ..write('primaryCalendar: $primaryCalendar, ') + ..write('selected: $selected, ') + ..write('hidden: $hidden, ') + ..write('readOnly: $readOnly, ') + ..write('backgroundColor: $backgroundColor, ') + ..write('foregroundColor: $foregroundColor, ') + ..write('colorId: $colorId, ') + ..write('timeZone: $timeZone, ') + ..write('accessRole: $accessRole, ') + ..write('isDeleted: $isDeleted, ') + ..write('rawJson: $rawJson, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + accountId, + provider, + providerCalendarId, + davCollectionId, + summary, + description, + primaryCalendar, + selected, + hidden, + readOnly, + backgroundColor, + foregroundColor, + colorId, + timeZone, + accessRole, + isDeleted, + rawJson, + createdAtLocal, + updatedAtLocal, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CalendarSource && + other.id == this.id && + other.accountId == this.accountId && + other.provider == this.provider && + other.providerCalendarId == this.providerCalendarId && + other.davCollectionId == this.davCollectionId && + other.summary == this.summary && + other.description == this.description && + other.primaryCalendar == this.primaryCalendar && + other.selected == this.selected && + other.hidden == this.hidden && + other.readOnly == this.readOnly && + other.backgroundColor == this.backgroundColor && + other.foregroundColor == this.foregroundColor && + other.colorId == this.colorId && + other.timeZone == this.timeZone && + other.accessRole == this.accessRole && + other.isDeleted == this.isDeleted && + other.rawJson == this.rawJson && + other.createdAtLocal == this.createdAtLocal && + other.updatedAtLocal == this.updatedAtLocal); +} + +class CalendarSourcesCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value provider; + final Value providerCalendarId; + final Value davCollectionId; + final Value summary; + final Value description; + final Value primaryCalendar; + final Value selected; + final Value hidden; + final Value readOnly; + final Value backgroundColor; + final Value foregroundColor; + final Value colorId; + final Value timeZone; + final Value accessRole; + final Value isDeleted; + final Value rawJson; + final Value createdAtLocal; + final Value updatedAtLocal; + final Value rowid; + const CalendarSourcesCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.provider = const Value.absent(), + this.providerCalendarId = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.summary = const Value.absent(), + this.description = const Value.absent(), + this.primaryCalendar = const Value.absent(), + this.selected = const Value.absent(), + this.hidden = const Value.absent(), + this.readOnly = const Value.absent(), + this.backgroundColor = const Value.absent(), + this.foregroundColor = const Value.absent(), + this.colorId = const Value.absent(), + this.timeZone = const Value.absent(), + this.accessRole = const Value.absent(), + this.isDeleted = const Value.absent(), + this.rawJson = const Value.absent(), + this.createdAtLocal = const Value.absent(), + this.updatedAtLocal = const Value.absent(), + this.rowid = const Value.absent(), + }); + CalendarSourcesCompanion.insert({ + required String id, + required String accountId, + required String provider, + required String providerCalendarId, + this.davCollectionId = const Value.absent(), + required String summary, + this.description = const Value.absent(), + this.primaryCalendar = const Value.absent(), + this.selected = const Value.absent(), + this.hidden = const Value.absent(), + this.readOnly = const Value.absent(), + this.backgroundColor = const Value.absent(), + this.foregroundColor = const Value.absent(), + this.colorId = const Value.absent(), + this.timeZone = const Value.absent(), + this.accessRole = const Value.absent(), + this.isDeleted = const Value.absent(), + this.rawJson = const Value.absent(), + required int createdAtLocal, + required int updatedAtLocal, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + provider = Value(provider), + providerCalendarId = Value(providerCalendarId), + summary = Value(summary), + createdAtLocal = Value(createdAtLocal), + updatedAtLocal = Value(updatedAtLocal); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? provider, + Expression? providerCalendarId, + Expression? davCollectionId, + Expression? summary, + Expression? description, + Expression? primaryCalendar, + Expression? selected, + Expression? hidden, + Expression? readOnly, + Expression? backgroundColor, + Expression? foregroundColor, + Expression? colorId, + Expression? timeZone, + Expression? accessRole, + Expression? isDeleted, + Expression? rawJson, + Expression? createdAtLocal, + Expression? updatedAtLocal, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (provider != null) 'provider': provider, + if (providerCalendarId != null) + 'provider_calendar_id': providerCalendarId, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (summary != null) 'summary': summary, + if (description != null) 'description': description, + if (primaryCalendar != null) 'primary_calendar': primaryCalendar, + if (selected != null) 'selected': selected, + if (hidden != null) 'hidden': hidden, + if (readOnly != null) 'read_only': readOnly, + if (backgroundColor != null) 'background_color': backgroundColor, + if (foregroundColor != null) 'foreground_color': foregroundColor, + if (colorId != null) 'color_id': colorId, + if (timeZone != null) 'time_zone': timeZone, + if (accessRole != null) 'access_role': accessRole, + if (isDeleted != null) 'is_deleted': isDeleted, + if (rawJson != null) 'raw_json': rawJson, + if (createdAtLocal != null) 'created_at_local': createdAtLocal, + if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, + if (rowid != null) 'rowid': rowid, + }); + } + + CalendarSourcesCompanion copyWith({ + Value? id, + Value? accountId, + Value? provider, + Value? providerCalendarId, + Value? davCollectionId, + Value? summary, + Value? description, + Value? primaryCalendar, + Value? selected, + Value? hidden, + Value? readOnly, + Value? backgroundColor, + Value? foregroundColor, + Value? colorId, + Value? timeZone, + Value? accessRole, + Value? isDeleted, + Value? rawJson, + Value? createdAtLocal, + Value? updatedAtLocal, + Value? rowid, + }) { + return CalendarSourcesCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + provider: provider ?? this.provider, + providerCalendarId: providerCalendarId ?? this.providerCalendarId, + davCollectionId: davCollectionId ?? this.davCollectionId, + summary: summary ?? this.summary, + description: description ?? this.description, + primaryCalendar: primaryCalendar ?? this.primaryCalendar, + selected: selected ?? this.selected, + hidden: hidden ?? this.hidden, + readOnly: readOnly ?? this.readOnly, + backgroundColor: backgroundColor ?? this.backgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + colorId: colorId ?? this.colorId, + timeZone: timeZone ?? this.timeZone, + accessRole: accessRole ?? this.accessRole, + isDeleted: isDeleted ?? this.isDeleted, + rawJson: rawJson ?? this.rawJson, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (providerCalendarId.present) { + map['provider_calendar_id'] = Variable(providerCalendarId.value); + } + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); + } + if (summary.present) { + map['summary'] = Variable(summary.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (primaryCalendar.present) { + map['primary_calendar'] = Variable(primaryCalendar.value); + } + if (selected.present) { + map['selected'] = Variable(selected.value); + } + if (hidden.present) { + map['hidden'] = Variable(hidden.value); + } + if (readOnly.present) { + map['read_only'] = Variable(readOnly.value); + } + if (backgroundColor.present) { + map['background_color'] = Variable(backgroundColor.value); + } + if (foregroundColor.present) { + map['foreground_color'] = Variable(foregroundColor.value); + } + if (colorId.present) { + map['color_id'] = Variable(colorId.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (accessRole.present) { + map['access_role'] = Variable(accessRole.value); + } + if (isDeleted.present) { + map['is_deleted'] = Variable(isDeleted.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (createdAtLocal.present) { + map['created_at_local'] = Variable(createdAtLocal.value); + } + if (updatedAtLocal.present) { + map['updated_at_local'] = Variable(updatedAtLocal.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CalendarSourcesCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('provider: $provider, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('summary: $summary, ') + ..write('description: $description, ') + ..write('primaryCalendar: $primaryCalendar, ') + ..write('selected: $selected, ') + ..write('hidden: $hidden, ') + ..write('readOnly: $readOnly, ') + ..write('backgroundColor: $backgroundColor, ') + ..write('foregroundColor: $foregroundColor, ') + ..write('colorId: $colorId, ') + ..write('timeZone: $timeZone, ') + ..write('accessRole: $accessRole, ') + ..write('isDeleted: $isDeleted, ') + ..write('rawJson: $rawJson, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CalendarEventsTable extends CalendarEvents + with TableInfo<$CalendarEventsTable, CalendarEvent> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CalendarEventsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _calendarSourceIdMeta = const VerificationMeta( + 'calendarSourceId', + ); + @override + late final GeneratedColumn calendarSourceId = GeneratedColumn( + 'calendar_source_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES calendar_sources (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _providerCalendarIdMeta = + const VerificationMeta('providerCalendarId'); + @override + late final GeneratedColumn providerCalendarId = + GeneratedColumn( + 'provider_calendar_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _providerEventIdMeta = const VerificationMeta( + 'providerEventId', + ); + @override + late final GeneratedColumn providerEventId = GeneratedColumn( + 'provider_event_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', + ); + @override + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _davObjectIdMeta = const VerificationMeta( + 'davObjectId', + ); + @override + late final GeneratedColumn davObjectId = GeneratedColumn( + 'dav_object_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_objects (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _davComponentIdMeta = const VerificationMeta( + 'davComponentId', + ); + @override + late final GeneratedColumn davComponentId = GeneratedColumn( + 'dav_component_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_object_components (id) ON DELETE SET NULL', + ), + ); + static const VerificationMeta _icalUidMeta = const VerificationMeta( + 'icalUid', + ); + @override + late final GeneratedColumn icalUid = GeneratedColumn( + 'ical_uid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _recurrenceIdKeyMeta = const VerificationMeta( + 'recurrenceIdKey', + ); + @override + late final GeneratedColumn recurrenceIdKey = GeneratedColumn( + 'recurrence_id_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _occurrenceKeyMeta = const VerificationMeta( + 'occurrenceKey', + ); + @override + late final GeneratedColumn occurrenceKey = GeneratedColumn( + 'occurrence_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _projectionVersionMeta = const VerificationMeta( + 'projectionVersion', + ); + @override + late final GeneratedColumn projectionVersion = GeneratedColumn( + 'projection_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); + static const VerificationMeta _providerRecurringEventIdMeta = + const VerificationMeta('providerRecurringEventId'); + @override + late final GeneratedColumn providerRecurringEventId = + GeneratedColumn( + 'provider_recurring_event_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _providerOriginalStartKeyMeta = + const VerificationMeta('providerOriginalStartKey'); + @override + late final GeneratedColumn providerOriginalStartKey = + GeneratedColumn( + 'provider_original_start_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _etagOrChangeKeyMeta = const VerificationMeta( + 'etagOrChangeKey', + ); + @override + late final GeneratedColumn etagOrChangeKey = GeneratedColumn( + 'etag_or_change_key', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _statusMeta = const VerificationMeta('status'); + @override + late final GeneratedColumn status = GeneratedColumn( + 'status', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _titleMeta = const VerificationMeta('title'); + @override + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', + ); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _locationMeta = const VerificationMeta( + 'location', + ); + @override + late final GeneratedColumn location = GeneratedColumn( + 'location', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _allDayMeta = const VerificationMeta('allDay'); + @override + late final GeneratedColumn allDay = GeneratedColumn( + 'all_day', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("all_day" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _startDateMeta = const VerificationMeta( + 'startDate', + ); + @override + late final GeneratedColumn startDate = GeneratedColumn( + 'start_date', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _startDateTimeMeta = const VerificationMeta( + 'startDateTime', + ); + @override + late final GeneratedColumn startDateTime = GeneratedColumn( + 'start_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _startTimeZoneMeta = const VerificationMeta( + 'startTimeZone', + ); + @override + late final GeneratedColumn startTimeZone = GeneratedColumn( + 'start_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _endDateMeta = const VerificationMeta( + 'endDate', + ); + @override + late final GeneratedColumn endDate = GeneratedColumn( + 'end_date', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _endDateTimeMeta = const VerificationMeta( + 'endDateTime', + ); + @override + late final GeneratedColumn endDateTime = GeneratedColumn( + 'end_date_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _endTimeZoneMeta = const VerificationMeta( + 'endTimeZone', + ); + @override + late final GeneratedColumn endTimeZone = GeneratedColumn( + 'end_time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _recurrenceJsonMeta = const VerificationMeta( + 'recurrenceJson', + ); + @override + late final GeneratedColumn recurrenceJson = GeneratedColumn( + 'recurrence_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _remindersJsonMeta = const VerificationMeta( + 'remindersJson', + ); + @override + late final GeneratedColumn remindersJson = GeneratedColumn( + 'reminders_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _attendeesJsonMeta = const VerificationMeta( + 'attendeesJson', + ); + @override + late final GeneratedColumn attendeesJson = GeneratedColumn( + 'attendees_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _categoriesJsonMeta = const VerificationMeta( + 'categoriesJson', + ); + @override + late final GeneratedColumn categoriesJson = GeneratedColumn( + 'categories_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _organizerJsonMeta = const VerificationMeta( + 'organizerJson', + ); + @override + late final GeneratedColumn organizerJson = GeneratedColumn( + 'organizer_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _creatorJsonMeta = const VerificationMeta( + 'creatorJson', + ); + @override + late final GeneratedColumn creatorJson = GeneratedColumn( + 'creator_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _colorIdMeta = const VerificationMeta( + 'colorId', + ); + @override + late final GeneratedColumn colorId = GeneratedColumn( + 'color_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _colorHexMeta = const VerificationMeta( + 'colorHex', + ); + @override + late final GeneratedColumn colorHex = GeneratedColumn( + 'color_hex', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _visibilityMeta = const VerificationMeta( + 'visibility', + ); + @override + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _transparencyOrShowAsMeta = + const VerificationMeta('transparencyOrShowAs'); + @override + late final GeneratedColumn transparencyOrShowAs = + GeneratedColumn( + 'transparency_or_show_as', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _eventTypeMeta = const VerificationMeta( + 'eventType', + ); + @override + late final GeneratedColumn eventType = GeneratedColumn( + 'event_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _webLinkMeta = const VerificationMeta( + 'webLink', + ); + @override + late final GeneratedColumn webLink = GeneratedColumn( + 'web_link', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _conferenceJsonMeta = const VerificationMeta( + 'conferenceJson', + ); + @override + late final GeneratedColumn conferenceJson = GeneratedColumn( + 'conference_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _attachmentsJsonMeta = const VerificationMeta( + 'attachmentsJson', + ); + @override + late final GeneratedColumn attachmentsJson = GeneratedColumn( + 'attachments_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _isCancelledMeta = const VerificationMeta( + 'isCancelled', + ); + @override + late final GeneratedColumn isCancelled = GeneratedColumn( + 'is_cancelled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_cancelled" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _isDeletedMeta = const VerificationMeta( + 'isDeleted', + ); + @override + late final GeneratedColumn isDeleted = GeneratedColumn( + 'is_deleted', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_deleted" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', + ); + @override + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtServerMeta = const VerificationMeta( + 'createdAtServer', + ); + @override + late final GeneratedColumn createdAtServer = GeneratedColumn( + 'created_at_server', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _updatedAtServerMeta = const VerificationMeta( + 'updatedAtServer', + ); + @override + late final GeneratedColumn updatedAtServer = GeneratedColumn( + 'updated_at_server', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( + 'createdAtLocal', + ); + @override + late final GeneratedColumn createdAtLocal = GeneratedColumn( + 'created_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( + 'updatedAtLocal', + ); + @override + late final GeneratedColumn updatedAtLocal = GeneratedColumn( + 'updated_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _syncStatusMeta = const VerificationMeta( + 'syncStatus', + ); + @override + late final GeneratedColumn syncStatus = GeneratedColumn( + 'sync_status', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('synced'), + ); + static const VerificationMeta _baselineRawJsonMeta = const VerificationMeta( + 'baselineRawJson', + ); + @override + late final GeneratedColumn baselineRawJson = GeneratedColumn( + 'baseline_raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + accountId, + calendarSourceId, + provider, + providerCalendarId, + providerEventId, + davCollectionId, + davObjectId, + davComponentId, + icalUid, + recurrenceIdKey, + occurrenceKey, + projectionVersion, + providerRecurringEventId, + providerOriginalStartKey, + etagOrChangeKey, + status, + title, + description, + location, + allDay, + startDate, + startDateTime, + startTimeZone, + endDate, + endDateTime, + endTimeZone, + recurrenceJson, + remindersJson, + attendeesJson, + categoriesJson, + organizerJson, + creatorJson, + colorId, + colorHex, + visibility, + transparencyOrShowAs, + eventType, + webLink, + conferenceJson, + attachmentsJson, + isCancelled, + isDeleted, + rawJson, + createdAtServer, + updatedAtServer, + createdAtLocal, + updatedAtLocal, + syncStatus, + baselineRawJson, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'calendar_events'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('calendar_source_id')) { + context.handle( + _calendarSourceIdMeta, + calendarSourceId.isAcceptableOrUnknown( + data['calendar_source_id']!, + _calendarSourceIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_calendarSourceIdMeta); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('provider_calendar_id')) { + context.handle( + _providerCalendarIdMeta, + providerCalendarId.isAcceptableOrUnknown( + data['provider_calendar_id']!, + _providerCalendarIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_providerCalendarIdMeta); + } + if (data.containsKey('provider_event_id')) { + context.handle( + _providerEventIdMeta, + providerEventId.isAcceptableOrUnknown( + data['provider_event_id']!, + _providerEventIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_providerEventIdMeta); + } + if (data.containsKey('dav_collection_id')) { + context.handle( + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), + ); + } + if (data.containsKey('dav_object_id')) { + context.handle( + _davObjectIdMeta, + davObjectId.isAcceptableOrUnknown( + data['dav_object_id']!, + _davObjectIdMeta, + ), + ); + } + if (data.containsKey('dav_component_id')) { + context.handle( + _davComponentIdMeta, + davComponentId.isAcceptableOrUnknown( + data['dav_component_id']!, + _davComponentIdMeta, + ), + ); + } + if (data.containsKey('ical_uid')) { + context.handle( + _icalUidMeta, + icalUid.isAcceptableOrUnknown(data['ical_uid']!, _icalUidMeta), + ); + } + if (data.containsKey('recurrence_id_key')) { + context.handle( + _recurrenceIdKeyMeta, + recurrenceIdKey.isAcceptableOrUnknown( + data['recurrence_id_key']!, + _recurrenceIdKeyMeta, + ), + ); + } + if (data.containsKey('occurrence_key')) { + context.handle( + _occurrenceKeyMeta, + occurrenceKey.isAcceptableOrUnknown( + data['occurrence_key']!, + _occurrenceKeyMeta, + ), + ); + } + if (data.containsKey('projection_version')) { + context.handle( + _projectionVersionMeta, + projectionVersion.isAcceptableOrUnknown( + data['projection_version']!, + _projectionVersionMeta, + ), + ); + } + if (data.containsKey('provider_recurring_event_id')) { + context.handle( + _providerRecurringEventIdMeta, + providerRecurringEventId.isAcceptableOrUnknown( + data['provider_recurring_event_id']!, + _providerRecurringEventIdMeta, + ), + ); + } + if (data.containsKey('provider_original_start_key')) { + context.handle( + _providerOriginalStartKeyMeta, + providerOriginalStartKey.isAcceptableOrUnknown( + data['provider_original_start_key']!, + _providerOriginalStartKeyMeta, + ), + ); + } + if (data.containsKey('etag_or_change_key')) { + context.handle( + _etagOrChangeKeyMeta, + etagOrChangeKey.isAcceptableOrUnknown( + data['etag_or_change_key']!, + _etagOrChangeKeyMeta, + ), + ); + } + if (data.containsKey('status')) { + context.handle( + _statusMeta, + status.isAcceptableOrUnknown(data['status']!, _statusMeta), + ); + } + if (data.containsKey('title')) { + context.handle( + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), + ); + } else if (isInserting) { + context.missing(_titleMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), + ); + } + if (data.containsKey('location')) { + context.handle( + _locationMeta, + location.isAcceptableOrUnknown(data['location']!, _locationMeta), + ); + } + if (data.containsKey('all_day')) { + context.handle( + _allDayMeta, + allDay.isAcceptableOrUnknown(data['all_day']!, _allDayMeta), + ); + } + if (data.containsKey('start_date')) { + context.handle( + _startDateMeta, + startDate.isAcceptableOrUnknown(data['start_date']!, _startDateMeta), + ); + } + if (data.containsKey('start_date_time')) { + context.handle( + _startDateTimeMeta, + startDateTime.isAcceptableOrUnknown( + data['start_date_time']!, + _startDateTimeMeta, + ), + ); + } + if (data.containsKey('start_time_zone')) { + context.handle( + _startTimeZoneMeta, + startTimeZone.isAcceptableOrUnknown( + data['start_time_zone']!, + _startTimeZoneMeta, + ), + ); + } + if (data.containsKey('end_date')) { + context.handle( + _endDateMeta, + endDate.isAcceptableOrUnknown(data['end_date']!, _endDateMeta), + ); + } + if (data.containsKey('end_date_time')) { + context.handle( + _endDateTimeMeta, + endDateTime.isAcceptableOrUnknown( + data['end_date_time']!, + _endDateTimeMeta, + ), + ); + } + if (data.containsKey('end_time_zone')) { + context.handle( + _endTimeZoneMeta, + endTimeZone.isAcceptableOrUnknown( + data['end_time_zone']!, + _endTimeZoneMeta, + ), + ); + } + if (data.containsKey('recurrence_json')) { + context.handle( + _recurrenceJsonMeta, + recurrenceJson.isAcceptableOrUnknown( + data['recurrence_json']!, + _recurrenceJsonMeta, + ), + ); + } + if (data.containsKey('reminders_json')) { + context.handle( + _remindersJsonMeta, + remindersJson.isAcceptableOrUnknown( + data['reminders_json']!, + _remindersJsonMeta, + ), + ); + } + if (data.containsKey('attendees_json')) { + context.handle( + _attendeesJsonMeta, + attendeesJson.isAcceptableOrUnknown( + data['attendees_json']!, + _attendeesJsonMeta, + ), + ); + } + if (data.containsKey('categories_json')) { + context.handle( + _categoriesJsonMeta, + categoriesJson.isAcceptableOrUnknown( + data['categories_json']!, + _categoriesJsonMeta, + ), + ); + } + if (data.containsKey('organizer_json')) { + context.handle( + _organizerJsonMeta, + organizerJson.isAcceptableOrUnknown( + data['organizer_json']!, + _organizerJsonMeta, + ), + ); + } + if (data.containsKey('creator_json')) { + context.handle( + _creatorJsonMeta, + creatorJson.isAcceptableOrUnknown( + data['creator_json']!, + _creatorJsonMeta, + ), + ); + } + if (data.containsKey('color_id')) { + context.handle( + _colorIdMeta, + colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), + ); + } + if (data.containsKey('color_hex')) { + context.handle( + _colorHexMeta, + colorHex.isAcceptableOrUnknown(data['color_hex']!, _colorHexMeta), + ); + } + if (data.containsKey('visibility')) { + context.handle( + _visibilityMeta, + visibility.isAcceptableOrUnknown(data['visibility']!, _visibilityMeta), + ); + } + if (data.containsKey('transparency_or_show_as')) { + context.handle( + _transparencyOrShowAsMeta, + transparencyOrShowAs.isAcceptableOrUnknown( + data['transparency_or_show_as']!, + _transparencyOrShowAsMeta, + ), + ); + } + if (data.containsKey('event_type')) { + context.handle( + _eventTypeMeta, + eventType.isAcceptableOrUnknown(data['event_type']!, _eventTypeMeta), + ); + } + if (data.containsKey('web_link')) { + context.handle( + _webLinkMeta, + webLink.isAcceptableOrUnknown(data['web_link']!, _webLinkMeta), + ); + } + if (data.containsKey('conference_json')) { + context.handle( + _conferenceJsonMeta, + conferenceJson.isAcceptableOrUnknown( + data['conference_json']!, + _conferenceJsonMeta, + ), + ); + } + if (data.containsKey('attachments_json')) { + context.handle( + _attachmentsJsonMeta, + attachmentsJson.isAcceptableOrUnknown( + data['attachments_json']!, + _attachmentsJsonMeta, + ), + ); + } + if (data.containsKey('is_cancelled')) { + context.handle( + _isCancelledMeta, + isCancelled.isAcceptableOrUnknown( + data['is_cancelled']!, + _isCancelledMeta, + ), + ); + } + if (data.containsKey('is_deleted')) { + context.handle( + _isDeletedMeta, + isDeleted.isAcceptableOrUnknown(data['is_deleted']!, _isDeletedMeta), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } + if (data.containsKey('created_at_server')) { + context.handle( + _createdAtServerMeta, + createdAtServer.isAcceptableOrUnknown( + data['created_at_server']!, + _createdAtServerMeta, + ), + ); + } + if (data.containsKey('updated_at_server')) { + context.handle( + _updatedAtServerMeta, + updatedAtServer.isAcceptableOrUnknown( + data['updated_at_server']!, + _updatedAtServerMeta, + ), + ); + } + if (data.containsKey('created_at_local')) { + context.handle( + _createdAtLocalMeta, + createdAtLocal.isAcceptableOrUnknown( + data['created_at_local']!, + _createdAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdAtLocalMeta); + } + if (data.containsKey('updated_at_local')) { + context.handle( + _updatedAtLocalMeta, + updatedAtLocal.isAcceptableOrUnknown( + data['updated_at_local']!, + _updatedAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedAtLocalMeta); + } + if (data.containsKey('sync_status')) { + context.handle( + _syncStatusMeta, + syncStatus.isAcceptableOrUnknown(data['sync_status']!, _syncStatusMeta), + ); + } + if (data.containsKey('baseline_raw_json')) { + context.handle( + _baselineRawJsonMeta, + baselineRawJson.isAcceptableOrUnknown( + data['baseline_raw_json']!, + _baselineRawJsonMeta, + ), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CalendarEvent map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CalendarEvent( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + calendarSourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}calendar_source_id'], + )!, + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + )!, + providerCalendarId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_calendar_id'], + )!, + providerEventId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_event_id'], + )!, + davCollectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_collection_id'], + ), + davObjectId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_object_id'], + ), + davComponentId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_component_id'], + ), + icalUid: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}ical_uid'], + ), + recurrenceIdKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}recurrence_id_key'], + ), + occurrenceKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}occurrence_key'], + ), + projectionVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}projection_version'], + )!, + providerRecurringEventId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_recurring_event_id'], + ), + providerOriginalStartKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider_original_start_key'], + ), + etagOrChangeKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}etag_or_change_key'], + ), + status: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}status'], + ), + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + location: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}location'], + ), + allDay: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}all_day'], + )!, + startDate: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}start_date'], + ), + startDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}start_date_time'], + ), + startTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}start_time_zone'], + ), + endDate: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}end_date'], + ), + endDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}end_date_time'], + ), + endTimeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}end_time_zone'], + ), + recurrenceJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}recurrence_json'], + ), + remindersJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}reminders_json'], + ), + attendeesJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}attendees_json'], + ), + categoriesJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}categories_json'], + ), + organizerJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}organizer_json'], + ), + creatorJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}creator_json'], + ), + colorId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color_id'], + ), + colorHex: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color_hex'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}visibility'], + ), + transparencyOrShowAs: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}transparency_or_show_as'], + ), + eventType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}event_type'], + ), + webLink: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}web_link'], + ), + conferenceJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}conference_json'], + ), + attachmentsJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}attachments_json'], + ), + isCancelled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_cancelled'], + )!, + isDeleted: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_deleted'], + )!, + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + ), + createdAtServer: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}created_at_server'], + ), + updatedAtServer: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}updated_at_server'], + ), + createdAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at_local'], + )!, + updatedAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at_local'], + )!, + syncStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_status'], + )!, + baselineRawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}baseline_raw_json'], + ), + ); + } + + @override + $CalendarEventsTable createAlias(String alias) { + return $CalendarEventsTable(attachedDatabase, alias); + } +} + +class CalendarEvent extends DataClass implements Insertable { + final String id; + final String accountId; + final String calendarSourceId; + final String provider; + final String providerCalendarId; + final String providerEventId; + final String? davCollectionId; + final String? davObjectId; + final String? davComponentId; + final String? icalUid; + final String? recurrenceIdKey; + final String? occurrenceKey; + final int projectionVersion; + final String? providerRecurringEventId; + final String? providerOriginalStartKey; + final String? etagOrChangeKey; + final String? status; + final String title; + final String? description; + final String? location; + final bool allDay; + final String? startDate; + final String? startDateTime; + final String? startTimeZone; + final String? endDate; + final String? endDateTime; + final String? endTimeZone; + final String? recurrenceJson; + final String? remindersJson; + final String? attendeesJson; + final String? categoriesJson; + final String? organizerJson; + final String? creatorJson; + final String? colorId; + final String? colorHex; + final String? visibility; + final String? transparencyOrShowAs; + final String? eventType; + final String? webLink; + final String? conferenceJson; + final String? attachmentsJson; + final bool isCancelled; + final bool isDeleted; + final String? rawJson; + final String? createdAtServer; + final String? updatedAtServer; + final int createdAtLocal; + final int updatedAtLocal; + final String syncStatus; + final String? baselineRawJson; + const CalendarEvent({ + required this.id, + required this.accountId, + required this.calendarSourceId, + required this.provider, + required this.providerCalendarId, + required this.providerEventId, + this.davCollectionId, + this.davObjectId, + this.davComponentId, + this.icalUid, + this.recurrenceIdKey, + this.occurrenceKey, + required this.projectionVersion, + this.providerRecurringEventId, + this.providerOriginalStartKey, + this.etagOrChangeKey, + this.status, + required this.title, + this.description, + this.location, + required this.allDay, + this.startDate, + this.startDateTime, + this.startTimeZone, + this.endDate, + this.endDateTime, + this.endTimeZone, + this.recurrenceJson, + this.remindersJson, + this.attendeesJson, + this.categoriesJson, + this.organizerJson, + this.creatorJson, + this.colorId, + this.colorHex, + this.visibility, + this.transparencyOrShowAs, + this.eventType, + this.webLink, + this.conferenceJson, + this.attachmentsJson, + required this.isCancelled, + required this.isDeleted, + this.rawJson, + this.createdAtServer, + this.updatedAtServer, + required this.createdAtLocal, + required this.updatedAtLocal, + required this.syncStatus, + this.baselineRawJson, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + map['calendar_source_id'] = Variable(calendarSourceId); + map['provider'] = Variable(provider); + map['provider_calendar_id'] = Variable(providerCalendarId); + map['provider_event_id'] = Variable(providerEventId); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); + } + if (!nullToAbsent || davObjectId != null) { + map['dav_object_id'] = Variable(davObjectId); + } + if (!nullToAbsent || davComponentId != null) { + map['dav_component_id'] = Variable(davComponentId); + } + if (!nullToAbsent || icalUid != null) { + map['ical_uid'] = Variable(icalUid); + } + if (!nullToAbsent || recurrenceIdKey != null) { + map['recurrence_id_key'] = Variable(recurrenceIdKey); + } + if (!nullToAbsent || occurrenceKey != null) { + map['occurrence_key'] = Variable(occurrenceKey); + } + map['projection_version'] = Variable(projectionVersion); + if (!nullToAbsent || providerRecurringEventId != null) { + map['provider_recurring_event_id'] = Variable( + providerRecurringEventId, + ); + } + if (!nullToAbsent || providerOriginalStartKey != null) { + map['provider_original_start_key'] = Variable( + providerOriginalStartKey, + ); + } + if (!nullToAbsent || etagOrChangeKey != null) { + map['etag_or_change_key'] = Variable(etagOrChangeKey); + } + if (!nullToAbsent || status != null) { + map['status'] = Variable(status); + } + map['title'] = Variable(title); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || location != null) { + map['location'] = Variable(location); + } + map['all_day'] = Variable(allDay); + if (!nullToAbsent || startDate != null) { + map['start_date'] = Variable(startDate); + } + if (!nullToAbsent || startDateTime != null) { + map['start_date_time'] = Variable(startDateTime); + } + if (!nullToAbsent || startTimeZone != null) { + map['start_time_zone'] = Variable(startTimeZone); + } + if (!nullToAbsent || endDate != null) { + map['end_date'] = Variable(endDate); + } + if (!nullToAbsent || endDateTime != null) { + map['end_date_time'] = Variable(endDateTime); + } + if (!nullToAbsent || endTimeZone != null) { + map['end_time_zone'] = Variable(endTimeZone); + } + if (!nullToAbsent || recurrenceJson != null) { + map['recurrence_json'] = Variable(recurrenceJson); + } + if (!nullToAbsent || remindersJson != null) { + map['reminders_json'] = Variable(remindersJson); + } + if (!nullToAbsent || attendeesJson != null) { + map['attendees_json'] = Variable(attendeesJson); + } + if (!nullToAbsent || categoriesJson != null) { + map['categories_json'] = Variable(categoriesJson); + } + if (!nullToAbsent || organizerJson != null) { + map['organizer_json'] = Variable(organizerJson); + } + if (!nullToAbsent || creatorJson != null) { + map['creator_json'] = Variable(creatorJson); + } + if (!nullToAbsent || colorId != null) { + map['color_id'] = Variable(colorId); + } + if (!nullToAbsent || colorHex != null) { + map['color_hex'] = Variable(colorHex); + } + if (!nullToAbsent || visibility != null) { + map['visibility'] = Variable(visibility); + } + if (!nullToAbsent || transparencyOrShowAs != null) { + map['transparency_or_show_as'] = Variable(transparencyOrShowAs); + } + if (!nullToAbsent || eventType != null) { + map['event_type'] = Variable(eventType); + } + if (!nullToAbsent || webLink != null) { + map['web_link'] = Variable(webLink); + } + if (!nullToAbsent || conferenceJson != null) { + map['conference_json'] = Variable(conferenceJson); + } + if (!nullToAbsent || attachmentsJson != null) { + map['attachments_json'] = Variable(attachmentsJson); + } + map['is_cancelled'] = Variable(isCancelled); + map['is_deleted'] = Variable(isDeleted); + if (!nullToAbsent || rawJson != null) { + map['raw_json'] = Variable(rawJson); + } + if (!nullToAbsent || createdAtServer != null) { + map['created_at_server'] = Variable(createdAtServer); + } + if (!nullToAbsent || updatedAtServer != null) { + map['updated_at_server'] = Variable(updatedAtServer); + } + map['created_at_local'] = Variable(createdAtLocal); + map['updated_at_local'] = Variable(updatedAtLocal); + map['sync_status'] = Variable(syncStatus); + if (!nullToAbsent || baselineRawJson != null) { + map['baseline_raw_json'] = Variable(baselineRawJson); + } + return map; + } + + CalendarEventsCompanion toCompanion(bool nullToAbsent) { + return CalendarEventsCompanion( + id: Value(id), + accountId: Value(accountId), + calendarSourceId: Value(calendarSourceId), + provider: Value(provider), + providerCalendarId: Value(providerCalendarId), + providerEventId: Value(providerEventId), + davCollectionId: davCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionId), + davObjectId: davObjectId == null && nullToAbsent + ? const Value.absent() + : Value(davObjectId), + davComponentId: davComponentId == null && nullToAbsent + ? const Value.absent() + : Value(davComponentId), + icalUid: icalUid == null && nullToAbsent + ? const Value.absent() + : Value(icalUid), + recurrenceIdKey: recurrenceIdKey == null && nullToAbsent + ? const Value.absent() + : Value(recurrenceIdKey), + occurrenceKey: occurrenceKey == null && nullToAbsent + ? const Value.absent() + : Value(occurrenceKey), + projectionVersion: Value(projectionVersion), + providerRecurringEventId: providerRecurringEventId == null && nullToAbsent + ? const Value.absent() + : Value(providerRecurringEventId), + providerOriginalStartKey: providerOriginalStartKey == null && nullToAbsent + ? const Value.absent() + : Value(providerOriginalStartKey), + etagOrChangeKey: etagOrChangeKey == null && nullToAbsent + ? const Value.absent() + : Value(etagOrChangeKey), + status: status == null && nullToAbsent + ? const Value.absent() + : Value(status), + title: Value(title), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + location: location == null && nullToAbsent + ? const Value.absent() + : Value(location), + allDay: Value(allDay), + startDate: startDate == null && nullToAbsent + ? const Value.absent() + : Value(startDate), + startDateTime: startDateTime == null && nullToAbsent + ? const Value.absent() + : Value(startDateTime), + startTimeZone: startTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(startTimeZone), + endDate: endDate == null && nullToAbsent + ? const Value.absent() + : Value(endDate), + endDateTime: endDateTime == null && nullToAbsent + ? const Value.absent() + : Value(endDateTime), + endTimeZone: endTimeZone == null && nullToAbsent + ? const Value.absent() + : Value(endTimeZone), + recurrenceJson: recurrenceJson == null && nullToAbsent + ? const Value.absent() + : Value(recurrenceJson), + remindersJson: remindersJson == null && nullToAbsent + ? const Value.absent() + : Value(remindersJson), + attendeesJson: attendeesJson == null && nullToAbsent + ? const Value.absent() + : Value(attendeesJson), + categoriesJson: categoriesJson == null && nullToAbsent + ? const Value.absent() + : Value(categoriesJson), + organizerJson: organizerJson == null && nullToAbsent + ? const Value.absent() + : Value(organizerJson), + creatorJson: creatorJson == null && nullToAbsent + ? const Value.absent() + : Value(creatorJson), + colorId: colorId == null && nullToAbsent + ? const Value.absent() + : Value(colorId), + colorHex: colorHex == null && nullToAbsent + ? const Value.absent() + : Value(colorHex), + visibility: visibility == null && nullToAbsent + ? const Value.absent() + : Value(visibility), + transparencyOrShowAs: transparencyOrShowAs == null && nullToAbsent + ? const Value.absent() + : Value(transparencyOrShowAs), + eventType: eventType == null && nullToAbsent + ? const Value.absent() + : Value(eventType), + webLink: webLink == null && nullToAbsent + ? const Value.absent() + : Value(webLink), + conferenceJson: conferenceJson == null && nullToAbsent + ? const Value.absent() + : Value(conferenceJson), + attachmentsJson: attachmentsJson == null && nullToAbsent + ? const Value.absent() + : Value(attachmentsJson), + isCancelled: Value(isCancelled), + isDeleted: Value(isDeleted), + rawJson: rawJson == null && nullToAbsent + ? const Value.absent() + : Value(rawJson), + createdAtServer: createdAtServer == null && nullToAbsent + ? const Value.absent() + : Value(createdAtServer), + updatedAtServer: updatedAtServer == null && nullToAbsent + ? const Value.absent() + : Value(updatedAtServer), + createdAtLocal: Value(createdAtLocal), + updatedAtLocal: Value(updatedAtLocal), + syncStatus: Value(syncStatus), + baselineRawJson: baselineRawJson == null && nullToAbsent + ? const Value.absent() + : Value(baselineRawJson), + ); + } + + factory CalendarEvent.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CalendarEvent( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + calendarSourceId: serializer.fromJson(json['calendarSourceId']), + provider: serializer.fromJson(json['provider']), + providerCalendarId: serializer.fromJson( + json['providerCalendarId'], + ), + providerEventId: serializer.fromJson(json['providerEventId']), + davCollectionId: serializer.fromJson(json['davCollectionId']), + davObjectId: serializer.fromJson(json['davObjectId']), + davComponentId: serializer.fromJson(json['davComponentId']), + icalUid: serializer.fromJson(json['icalUid']), + recurrenceIdKey: serializer.fromJson(json['recurrenceIdKey']), + occurrenceKey: serializer.fromJson(json['occurrenceKey']), + projectionVersion: serializer.fromJson(json['projectionVersion']), + providerRecurringEventId: serializer.fromJson( + json['providerRecurringEventId'], + ), + providerOriginalStartKey: serializer.fromJson( + json['providerOriginalStartKey'], + ), + etagOrChangeKey: serializer.fromJson(json['etagOrChangeKey']), + status: serializer.fromJson(json['status']), + title: serializer.fromJson(json['title']), + description: serializer.fromJson(json['description']), + location: serializer.fromJson(json['location']), + allDay: serializer.fromJson(json['allDay']), + startDate: serializer.fromJson(json['startDate']), + startDateTime: serializer.fromJson(json['startDateTime']), + startTimeZone: serializer.fromJson(json['startTimeZone']), + endDate: serializer.fromJson(json['endDate']), + endDateTime: serializer.fromJson(json['endDateTime']), + endTimeZone: serializer.fromJson(json['endTimeZone']), + recurrenceJson: serializer.fromJson(json['recurrenceJson']), + remindersJson: serializer.fromJson(json['remindersJson']), + attendeesJson: serializer.fromJson(json['attendeesJson']), + categoriesJson: serializer.fromJson(json['categoriesJson']), + organizerJson: serializer.fromJson(json['organizerJson']), + creatorJson: serializer.fromJson(json['creatorJson']), + colorId: serializer.fromJson(json['colorId']), + colorHex: serializer.fromJson(json['colorHex']), + visibility: serializer.fromJson(json['visibility']), + transparencyOrShowAs: serializer.fromJson( + json['transparencyOrShowAs'], + ), + eventType: serializer.fromJson(json['eventType']), + webLink: serializer.fromJson(json['webLink']), + conferenceJson: serializer.fromJson(json['conferenceJson']), + attachmentsJson: serializer.fromJson(json['attachmentsJson']), + isCancelled: serializer.fromJson(json['isCancelled']), + isDeleted: serializer.fromJson(json['isDeleted']), + rawJson: serializer.fromJson(json['rawJson']), + createdAtServer: serializer.fromJson(json['createdAtServer']), + updatedAtServer: serializer.fromJson(json['updatedAtServer']), + createdAtLocal: serializer.fromJson(json['createdAtLocal']), + updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + syncStatus: serializer.fromJson(json['syncStatus']), + baselineRawJson: serializer.fromJson(json['baselineRawJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'calendarSourceId': serializer.toJson(calendarSourceId), + 'provider': serializer.toJson(provider), + 'providerCalendarId': serializer.toJson(providerCalendarId), + 'providerEventId': serializer.toJson(providerEventId), + 'davCollectionId': serializer.toJson(davCollectionId), + 'davObjectId': serializer.toJson(davObjectId), + 'davComponentId': serializer.toJson(davComponentId), + 'icalUid': serializer.toJson(icalUid), + 'recurrenceIdKey': serializer.toJson(recurrenceIdKey), + 'occurrenceKey': serializer.toJson(occurrenceKey), + 'projectionVersion': serializer.toJson(projectionVersion), + 'providerRecurringEventId': serializer.toJson( + providerRecurringEventId, + ), + 'providerOriginalStartKey': serializer.toJson( + providerOriginalStartKey, + ), + 'etagOrChangeKey': serializer.toJson(etagOrChangeKey), + 'status': serializer.toJson(status), + 'title': serializer.toJson(title), + 'description': serializer.toJson(description), + 'location': serializer.toJson(location), + 'allDay': serializer.toJson(allDay), + 'startDate': serializer.toJson(startDate), + 'startDateTime': serializer.toJson(startDateTime), + 'startTimeZone': serializer.toJson(startTimeZone), + 'endDate': serializer.toJson(endDate), + 'endDateTime': serializer.toJson(endDateTime), + 'endTimeZone': serializer.toJson(endTimeZone), + 'recurrenceJson': serializer.toJson(recurrenceJson), + 'remindersJson': serializer.toJson(remindersJson), + 'attendeesJson': serializer.toJson(attendeesJson), + 'categoriesJson': serializer.toJson(categoriesJson), + 'organizerJson': serializer.toJson(organizerJson), + 'creatorJson': serializer.toJson(creatorJson), + 'colorId': serializer.toJson(colorId), + 'colorHex': serializer.toJson(colorHex), + 'visibility': serializer.toJson(visibility), + 'transparencyOrShowAs': serializer.toJson(transparencyOrShowAs), + 'eventType': serializer.toJson(eventType), + 'webLink': serializer.toJson(webLink), + 'conferenceJson': serializer.toJson(conferenceJson), + 'attachmentsJson': serializer.toJson(attachmentsJson), + 'isCancelled': serializer.toJson(isCancelled), + 'isDeleted': serializer.toJson(isDeleted), + 'rawJson': serializer.toJson(rawJson), + 'createdAtServer': serializer.toJson(createdAtServer), + 'updatedAtServer': serializer.toJson(updatedAtServer), + 'createdAtLocal': serializer.toJson(createdAtLocal), + 'updatedAtLocal': serializer.toJson(updatedAtLocal), + 'syncStatus': serializer.toJson(syncStatus), + 'baselineRawJson': serializer.toJson(baselineRawJson), + }; + } + + CalendarEvent copyWith({ + String? id, + String? accountId, + String? calendarSourceId, + String? provider, + String? providerCalendarId, + String? providerEventId, + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value occurrenceKey = const Value.absent(), + int? projectionVersion, + Value providerRecurringEventId = const Value.absent(), + Value providerOriginalStartKey = const Value.absent(), + Value etagOrChangeKey = const Value.absent(), + Value status = const Value.absent(), + String? title, + Value description = const Value.absent(), + Value location = const Value.absent(), + bool? allDay, + Value startDate = const Value.absent(), + Value startDateTime = const Value.absent(), + Value startTimeZone = const Value.absent(), + Value endDate = const Value.absent(), + Value endDateTime = const Value.absent(), + Value endTimeZone = const Value.absent(), + Value recurrenceJson = const Value.absent(), + Value remindersJson = const Value.absent(), + Value attendeesJson = const Value.absent(), + Value categoriesJson = const Value.absent(), + Value organizerJson = const Value.absent(), + Value creatorJson = const Value.absent(), + Value colorId = const Value.absent(), + Value colorHex = const Value.absent(), + Value visibility = const Value.absent(), + Value transparencyOrShowAs = const Value.absent(), + Value eventType = const Value.absent(), + Value webLink = const Value.absent(), + Value conferenceJson = const Value.absent(), + Value attachmentsJson = const Value.absent(), + bool? isCancelled, + bool? isDeleted, + Value rawJson = const Value.absent(), + Value createdAtServer = const Value.absent(), + Value updatedAtServer = const Value.absent(), + int? createdAtLocal, + int? updatedAtLocal, + String? syncStatus, + Value baselineRawJson = const Value.absent(), + }) => CalendarEvent( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + calendarSourceId: calendarSourceId ?? this.calendarSourceId, + provider: provider ?? this.provider, + providerCalendarId: providerCalendarId ?? this.providerCalendarId, + providerEventId: providerEventId ?? this.providerEventId, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + davObjectId: davObjectId.present ? davObjectId.value : this.davObjectId, + davComponentId: davComponentId.present + ? davComponentId.value + : this.davComponentId, + icalUid: icalUid.present ? icalUid.value : this.icalUid, + recurrenceIdKey: recurrenceIdKey.present + ? recurrenceIdKey.value + : this.recurrenceIdKey, + occurrenceKey: occurrenceKey.present + ? occurrenceKey.value + : this.occurrenceKey, + projectionVersion: projectionVersion ?? this.projectionVersion, + providerRecurringEventId: providerRecurringEventId.present + ? providerRecurringEventId.value + : this.providerRecurringEventId, + providerOriginalStartKey: providerOriginalStartKey.present + ? providerOriginalStartKey.value + : this.providerOriginalStartKey, + etagOrChangeKey: etagOrChangeKey.present + ? etagOrChangeKey.value + : this.etagOrChangeKey, + status: status.present ? status.value : this.status, + title: title ?? this.title, + description: description.present ? description.value : this.description, + location: location.present ? location.value : this.location, + allDay: allDay ?? this.allDay, + startDate: startDate.present ? startDate.value : this.startDate, + startDateTime: startDateTime.present + ? startDateTime.value + : this.startDateTime, + startTimeZone: startTimeZone.present + ? startTimeZone.value + : this.startTimeZone, + endDate: endDate.present ? endDate.value : this.endDate, + endDateTime: endDateTime.present ? endDateTime.value : this.endDateTime, + endTimeZone: endTimeZone.present ? endTimeZone.value : this.endTimeZone, + recurrenceJson: recurrenceJson.present + ? recurrenceJson.value + : this.recurrenceJson, + remindersJson: remindersJson.present + ? remindersJson.value + : this.remindersJson, + attendeesJson: attendeesJson.present + ? attendeesJson.value + : this.attendeesJson, + categoriesJson: categoriesJson.present + ? categoriesJson.value + : this.categoriesJson, + organizerJson: organizerJson.present + ? organizerJson.value + : this.organizerJson, + creatorJson: creatorJson.present ? creatorJson.value : this.creatorJson, + colorId: colorId.present ? colorId.value : this.colorId, + colorHex: colorHex.present ? colorHex.value : this.colorHex, + visibility: visibility.present ? visibility.value : this.visibility, + transparencyOrShowAs: transparencyOrShowAs.present + ? transparencyOrShowAs.value + : this.transparencyOrShowAs, + eventType: eventType.present ? eventType.value : this.eventType, + webLink: webLink.present ? webLink.value : this.webLink, + conferenceJson: conferenceJson.present + ? conferenceJson.value + : this.conferenceJson, + attachmentsJson: attachmentsJson.present + ? attachmentsJson.value + : this.attachmentsJson, + isCancelled: isCancelled ?? this.isCancelled, + isDeleted: isDeleted ?? this.isDeleted, + rawJson: rawJson.present ? rawJson.value : this.rawJson, + createdAtServer: createdAtServer.present + ? createdAtServer.value + : this.createdAtServer, + updatedAtServer: updatedAtServer.present + ? updatedAtServer.value + : this.updatedAtServer, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + syncStatus: syncStatus ?? this.syncStatus, + baselineRawJson: baselineRawJson.present + ? baselineRawJson.value + : this.baselineRawJson, + ); + CalendarEvent copyWithCompanion(CalendarEventsCompanion data) { + return CalendarEvent( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + calendarSourceId: data.calendarSourceId.present + ? data.calendarSourceId.value + : this.calendarSourceId, + provider: data.provider.present ? data.provider.value : this.provider, + providerCalendarId: data.providerCalendarId.present + ? data.providerCalendarId.value + : this.providerCalendarId, + providerEventId: data.providerEventId.present + ? data.providerEventId.value + : this.providerEventId, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + davObjectId: data.davObjectId.present + ? data.davObjectId.value + : this.davObjectId, + davComponentId: data.davComponentId.present + ? data.davComponentId.value + : this.davComponentId, + icalUid: data.icalUid.present ? data.icalUid.value : this.icalUid, + recurrenceIdKey: data.recurrenceIdKey.present + ? data.recurrenceIdKey.value + : this.recurrenceIdKey, + occurrenceKey: data.occurrenceKey.present + ? data.occurrenceKey.value + : this.occurrenceKey, + projectionVersion: data.projectionVersion.present + ? data.projectionVersion.value + : this.projectionVersion, + providerRecurringEventId: data.providerRecurringEventId.present + ? data.providerRecurringEventId.value + : this.providerRecurringEventId, + providerOriginalStartKey: data.providerOriginalStartKey.present + ? data.providerOriginalStartKey.value + : this.providerOriginalStartKey, + etagOrChangeKey: data.etagOrChangeKey.present + ? data.etagOrChangeKey.value + : this.etagOrChangeKey, + status: data.status.present ? data.status.value : this.status, + title: data.title.present ? data.title.value : this.title, + description: data.description.present + ? data.description.value + : this.description, + location: data.location.present ? data.location.value : this.location, + allDay: data.allDay.present ? data.allDay.value : this.allDay, + startDate: data.startDate.present ? data.startDate.value : this.startDate, + startDateTime: data.startDateTime.present + ? data.startDateTime.value + : this.startDateTime, + startTimeZone: data.startTimeZone.present + ? data.startTimeZone.value + : this.startTimeZone, + endDate: data.endDate.present ? data.endDate.value : this.endDate, + endDateTime: data.endDateTime.present + ? data.endDateTime.value + : this.endDateTime, + endTimeZone: data.endTimeZone.present + ? data.endTimeZone.value + : this.endTimeZone, + recurrenceJson: data.recurrenceJson.present + ? data.recurrenceJson.value + : this.recurrenceJson, + remindersJson: data.remindersJson.present + ? data.remindersJson.value + : this.remindersJson, + attendeesJson: data.attendeesJson.present + ? data.attendeesJson.value + : this.attendeesJson, + categoriesJson: data.categoriesJson.present + ? data.categoriesJson.value + : this.categoriesJson, + organizerJson: data.organizerJson.present + ? data.organizerJson.value + : this.organizerJson, + creatorJson: data.creatorJson.present + ? data.creatorJson.value + : this.creatorJson, + colorId: data.colorId.present ? data.colorId.value : this.colorId, + colorHex: data.colorHex.present ? data.colorHex.value : this.colorHex, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + transparencyOrShowAs: data.transparencyOrShowAs.present + ? data.transparencyOrShowAs.value + : this.transparencyOrShowAs, + eventType: data.eventType.present ? data.eventType.value : this.eventType, + webLink: data.webLink.present ? data.webLink.value : this.webLink, + conferenceJson: data.conferenceJson.present + ? data.conferenceJson.value + : this.conferenceJson, + attachmentsJson: data.attachmentsJson.present + ? data.attachmentsJson.value + : this.attachmentsJson, + isCancelled: data.isCancelled.present + ? data.isCancelled.value + : this.isCancelled, + isDeleted: data.isDeleted.present ? data.isDeleted.value : this.isDeleted, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + createdAtServer: data.createdAtServer.present + ? data.createdAtServer.value + : this.createdAtServer, + updatedAtServer: data.updatedAtServer.present + ? data.updatedAtServer.value + : this.updatedAtServer, + createdAtLocal: data.createdAtLocal.present + ? data.createdAtLocal.value + : this.createdAtLocal, + updatedAtLocal: data.updatedAtLocal.present + ? data.updatedAtLocal.value + : this.updatedAtLocal, + syncStatus: data.syncStatus.present + ? data.syncStatus.value + : this.syncStatus, + baselineRawJson: data.baselineRawJson.present + ? data.baselineRawJson.value + : this.baselineRawJson, + ); + } + + @override + String toString() { + return (StringBuffer('CalendarEvent(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('calendarSourceId: $calendarSourceId, ') + ..write('provider: $provider, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('providerEventId: $providerEventId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('davComponentId: $davComponentId, ') + ..write('icalUid: $icalUid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('occurrenceKey: $occurrenceKey, ') + ..write('projectionVersion: $projectionVersion, ') + ..write('providerRecurringEventId: $providerRecurringEventId, ') + ..write('providerOriginalStartKey: $providerOriginalStartKey, ') + ..write('etagOrChangeKey: $etagOrChangeKey, ') + ..write('status: $status, ') + ..write('title: $title, ') + ..write('description: $description, ') + ..write('location: $location, ') + ..write('allDay: $allDay, ') + ..write('startDate: $startDate, ') + ..write('startDateTime: $startDateTime, ') + ..write('startTimeZone: $startTimeZone, ') + ..write('endDate: $endDate, ') + ..write('endDateTime: $endDateTime, ') + ..write('endTimeZone: $endTimeZone, ') + ..write('recurrenceJson: $recurrenceJson, ') + ..write('remindersJson: $remindersJson, ') + ..write('attendeesJson: $attendeesJson, ') + ..write('categoriesJson: $categoriesJson, ') + ..write('organizerJson: $organizerJson, ') + ..write('creatorJson: $creatorJson, ') + ..write('colorId: $colorId, ') + ..write('colorHex: $colorHex, ') + ..write('visibility: $visibility, ') + ..write('transparencyOrShowAs: $transparencyOrShowAs, ') + ..write('eventType: $eventType, ') + ..write('webLink: $webLink, ') + ..write('conferenceJson: $conferenceJson, ') + ..write('attachmentsJson: $attachmentsJson, ') + ..write('isCancelled: $isCancelled, ') + ..write('isDeleted: $isDeleted, ') + ..write('rawJson: $rawJson, ') + ..write('createdAtServer: $createdAtServer, ') + ..write('updatedAtServer: $updatedAtServer, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('syncStatus: $syncStatus, ') + ..write('baselineRawJson: $baselineRawJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + accountId, + calendarSourceId, + provider, + providerCalendarId, + providerEventId, + davCollectionId, + davObjectId, + davComponentId, + icalUid, + recurrenceIdKey, + occurrenceKey, + projectionVersion, + providerRecurringEventId, + providerOriginalStartKey, + etagOrChangeKey, + status, + title, + description, + location, + allDay, + startDate, + startDateTime, + startTimeZone, + endDate, + endDateTime, + endTimeZone, + recurrenceJson, + remindersJson, + attendeesJson, + categoriesJson, + organizerJson, + creatorJson, + colorId, + colorHex, + visibility, + transparencyOrShowAs, + eventType, + webLink, + conferenceJson, + attachmentsJson, + isCancelled, + isDeleted, + rawJson, + createdAtServer, + updatedAtServer, + createdAtLocal, + updatedAtLocal, + syncStatus, + baselineRawJson, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CalendarEvent && + other.id == this.id && + other.accountId == this.accountId && + other.calendarSourceId == this.calendarSourceId && + other.provider == this.provider && + other.providerCalendarId == this.providerCalendarId && + other.providerEventId == this.providerEventId && + other.davCollectionId == this.davCollectionId && + other.davObjectId == this.davObjectId && + other.davComponentId == this.davComponentId && + other.icalUid == this.icalUid && + other.recurrenceIdKey == this.recurrenceIdKey && + other.occurrenceKey == this.occurrenceKey && + other.projectionVersion == this.projectionVersion && + other.providerRecurringEventId == this.providerRecurringEventId && + other.providerOriginalStartKey == this.providerOriginalStartKey && + other.etagOrChangeKey == this.etagOrChangeKey && + other.status == this.status && + other.title == this.title && + other.description == this.description && + other.location == this.location && + other.allDay == this.allDay && + other.startDate == this.startDate && + other.startDateTime == this.startDateTime && + other.startTimeZone == this.startTimeZone && + other.endDate == this.endDate && + other.endDateTime == this.endDateTime && + other.endTimeZone == this.endTimeZone && + other.recurrenceJson == this.recurrenceJson && + other.remindersJson == this.remindersJson && + other.attendeesJson == this.attendeesJson && + other.categoriesJson == this.categoriesJson && + other.organizerJson == this.organizerJson && + other.creatorJson == this.creatorJson && + other.colorId == this.colorId && + other.colorHex == this.colorHex && + other.visibility == this.visibility && + other.transparencyOrShowAs == this.transparencyOrShowAs && + other.eventType == this.eventType && + other.webLink == this.webLink && + other.conferenceJson == this.conferenceJson && + other.attachmentsJson == this.attachmentsJson && + other.isCancelled == this.isCancelled && + other.isDeleted == this.isDeleted && + other.rawJson == this.rawJson && + other.createdAtServer == this.createdAtServer && + other.updatedAtServer == this.updatedAtServer && + other.createdAtLocal == this.createdAtLocal && + other.updatedAtLocal == this.updatedAtLocal && + other.syncStatus == this.syncStatus && + other.baselineRawJson == this.baselineRawJson); +} + +class CalendarEventsCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value calendarSourceId; + final Value provider; + final Value providerCalendarId; + final Value providerEventId; + final Value davCollectionId; + final Value davObjectId; + final Value davComponentId; + final Value icalUid; + final Value recurrenceIdKey; + final Value occurrenceKey; + final Value projectionVersion; + final Value providerRecurringEventId; + final Value providerOriginalStartKey; + final Value etagOrChangeKey; + final Value status; + final Value title; + final Value description; + final Value location; + final Value allDay; + final Value startDate; + final Value startDateTime; + final Value startTimeZone; + final Value endDate; + final Value endDateTime; + final Value endTimeZone; + final Value recurrenceJson; + final Value remindersJson; + final Value attendeesJson; + final Value categoriesJson; + final Value organizerJson; + final Value creatorJson; + final Value colorId; + final Value colorHex; + final Value visibility; + final Value transparencyOrShowAs; + final Value eventType; + final Value webLink; + final Value conferenceJson; + final Value attachmentsJson; + final Value isCancelled; + final Value isDeleted; + final Value rawJson; + final Value createdAtServer; + final Value updatedAtServer; + final Value createdAtLocal; + final Value updatedAtLocal; + final Value syncStatus; + final Value baselineRawJson; + final Value rowid; + const CalendarEventsCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.calendarSourceId = const Value.absent(), + this.provider = const Value.absent(), + this.providerCalendarId = const Value.absent(), + this.providerEventId = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davComponentId = const Value.absent(), + this.icalUid = const Value.absent(), + this.recurrenceIdKey = const Value.absent(), + this.occurrenceKey = const Value.absent(), + this.projectionVersion = const Value.absent(), + this.providerRecurringEventId = const Value.absent(), + this.providerOriginalStartKey = const Value.absent(), + this.etagOrChangeKey = const Value.absent(), + this.status = const Value.absent(), + this.title = const Value.absent(), + this.description = const Value.absent(), + this.location = const Value.absent(), + this.allDay = const Value.absent(), + this.startDate = const Value.absent(), + this.startDateTime = const Value.absent(), + this.startTimeZone = const Value.absent(), + this.endDate = const Value.absent(), + this.endDateTime = const Value.absent(), + this.endTimeZone = const Value.absent(), + this.recurrenceJson = const Value.absent(), + this.remindersJson = const Value.absent(), + this.attendeesJson = const Value.absent(), + this.categoriesJson = const Value.absent(), + this.organizerJson = const Value.absent(), + this.creatorJson = const Value.absent(), + this.colorId = const Value.absent(), + this.colorHex = const Value.absent(), + this.visibility = const Value.absent(), + this.transparencyOrShowAs = const Value.absent(), + this.eventType = const Value.absent(), + this.webLink = const Value.absent(), + this.conferenceJson = const Value.absent(), + this.attachmentsJson = const Value.absent(), + this.isCancelled = const Value.absent(), + this.isDeleted = const Value.absent(), + this.rawJson = const Value.absent(), + this.createdAtServer = const Value.absent(), + this.updatedAtServer = const Value.absent(), + this.createdAtLocal = const Value.absent(), + this.updatedAtLocal = const Value.absent(), + this.syncStatus = const Value.absent(), + this.baselineRawJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + CalendarEventsCompanion.insert({ + required String id, + required String accountId, + required String calendarSourceId, + required String provider, + required String providerCalendarId, + required String providerEventId, + this.davCollectionId = const Value.absent(), + this.davObjectId = const Value.absent(), + this.davComponentId = const Value.absent(), + this.icalUid = const Value.absent(), + this.recurrenceIdKey = const Value.absent(), + this.occurrenceKey = const Value.absent(), + this.projectionVersion = const Value.absent(), + this.providerRecurringEventId = const Value.absent(), + this.providerOriginalStartKey = const Value.absent(), + this.etagOrChangeKey = const Value.absent(), + this.status = const Value.absent(), + required String title, + this.description = const Value.absent(), + this.location = const Value.absent(), + this.allDay = const Value.absent(), + this.startDate = const Value.absent(), + this.startDateTime = const Value.absent(), + this.startTimeZone = const Value.absent(), + this.endDate = const Value.absent(), + this.endDateTime = const Value.absent(), + this.endTimeZone = const Value.absent(), + this.recurrenceJson = const Value.absent(), + this.remindersJson = const Value.absent(), + this.attendeesJson = const Value.absent(), + this.categoriesJson = const Value.absent(), + this.organizerJson = const Value.absent(), + this.creatorJson = const Value.absent(), + this.colorId = const Value.absent(), + this.colorHex = const Value.absent(), + this.visibility = const Value.absent(), + this.transparencyOrShowAs = const Value.absent(), + this.eventType = const Value.absent(), + this.webLink = const Value.absent(), + this.conferenceJson = const Value.absent(), + this.attachmentsJson = const Value.absent(), + this.isCancelled = const Value.absent(), + this.isDeleted = const Value.absent(), + this.rawJson = const Value.absent(), + this.createdAtServer = const Value.absent(), + this.updatedAtServer = const Value.absent(), + required int createdAtLocal, + required int updatedAtLocal, + this.syncStatus = const Value.absent(), + this.baselineRawJson = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + calendarSourceId = Value(calendarSourceId), + provider = Value(provider), + providerCalendarId = Value(providerCalendarId), + providerEventId = Value(providerEventId), + title = Value(title), + createdAtLocal = Value(createdAtLocal), + updatedAtLocal = Value(updatedAtLocal); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? calendarSourceId, + Expression? provider, + Expression? providerCalendarId, + Expression? providerEventId, + Expression? davCollectionId, + Expression? davObjectId, + Expression? davComponentId, + Expression? icalUid, + Expression? recurrenceIdKey, + Expression? occurrenceKey, + Expression? projectionVersion, + Expression? providerRecurringEventId, + Expression? providerOriginalStartKey, + Expression? etagOrChangeKey, + Expression? status, + Expression? title, + Expression? description, + Expression? location, + Expression? allDay, + Expression? startDate, + Expression? startDateTime, + Expression? startTimeZone, + Expression? endDate, + Expression? endDateTime, + Expression? endTimeZone, + Expression? recurrenceJson, + Expression? remindersJson, + Expression? attendeesJson, + Expression? categoriesJson, + Expression? organizerJson, + Expression? creatorJson, + Expression? colorId, + Expression? colorHex, + Expression? visibility, + Expression? transparencyOrShowAs, + Expression? eventType, + Expression? webLink, + Expression? conferenceJson, + Expression? attachmentsJson, + Expression? isCancelled, + Expression? isDeleted, + Expression? rawJson, + Expression? createdAtServer, + Expression? updatedAtServer, + Expression? createdAtLocal, + Expression? updatedAtLocal, + Expression? syncStatus, + Expression? baselineRawJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (calendarSourceId != null) 'calendar_source_id': calendarSourceId, + if (provider != null) 'provider': provider, + if (providerCalendarId != null) + 'provider_calendar_id': providerCalendarId, + if (providerEventId != null) 'provider_event_id': providerEventId, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (davObjectId != null) 'dav_object_id': davObjectId, + if (davComponentId != null) 'dav_component_id': davComponentId, + if (icalUid != null) 'ical_uid': icalUid, + if (recurrenceIdKey != null) 'recurrence_id_key': recurrenceIdKey, + if (occurrenceKey != null) 'occurrence_key': occurrenceKey, + if (projectionVersion != null) 'projection_version': projectionVersion, + if (providerRecurringEventId != null) + 'provider_recurring_event_id': providerRecurringEventId, + if (providerOriginalStartKey != null) + 'provider_original_start_key': providerOriginalStartKey, + if (etagOrChangeKey != null) 'etag_or_change_key': etagOrChangeKey, + if (status != null) 'status': status, + if (title != null) 'title': title, + if (description != null) 'description': description, + if (location != null) 'location': location, + if (allDay != null) 'all_day': allDay, + if (startDate != null) 'start_date': startDate, + if (startDateTime != null) 'start_date_time': startDateTime, + if (startTimeZone != null) 'start_time_zone': startTimeZone, + if (endDate != null) 'end_date': endDate, + if (endDateTime != null) 'end_date_time': endDateTime, + if (endTimeZone != null) 'end_time_zone': endTimeZone, + if (recurrenceJson != null) 'recurrence_json': recurrenceJson, + if (remindersJson != null) 'reminders_json': remindersJson, + if (attendeesJson != null) 'attendees_json': attendeesJson, + if (categoriesJson != null) 'categories_json': categoriesJson, + if (organizerJson != null) 'organizer_json': organizerJson, + if (creatorJson != null) 'creator_json': creatorJson, + if (colorId != null) 'color_id': colorId, + if (colorHex != null) 'color_hex': colorHex, + if (visibility != null) 'visibility': visibility, + if (transparencyOrShowAs != null) + 'transparency_or_show_as': transparencyOrShowAs, + if (eventType != null) 'event_type': eventType, + if (webLink != null) 'web_link': webLink, + if (conferenceJson != null) 'conference_json': conferenceJson, + if (attachmentsJson != null) 'attachments_json': attachmentsJson, + if (isCancelled != null) 'is_cancelled': isCancelled, + if (isDeleted != null) 'is_deleted': isDeleted, + if (rawJson != null) 'raw_json': rawJson, + if (createdAtServer != null) 'created_at_server': createdAtServer, + if (updatedAtServer != null) 'updated_at_server': updatedAtServer, + if (createdAtLocal != null) 'created_at_local': createdAtLocal, + if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, + if (syncStatus != null) 'sync_status': syncStatus, + if (baselineRawJson != null) 'baseline_raw_json': baselineRawJson, + if (rowid != null) 'rowid': rowid, + }); + } + + CalendarEventsCompanion copyWith({ + Value? id, + Value? accountId, + Value? calendarSourceId, + Value? provider, + Value? providerCalendarId, + Value? providerEventId, + Value? davCollectionId, + Value? davObjectId, + Value? davComponentId, + Value? icalUid, + Value? recurrenceIdKey, + Value? occurrenceKey, + Value? projectionVersion, + Value? providerRecurringEventId, + Value? providerOriginalStartKey, + Value? etagOrChangeKey, + Value? status, + Value? title, + Value? description, + Value? location, + Value? allDay, + Value? startDate, + Value? startDateTime, + Value? startTimeZone, + Value? endDate, + Value? endDateTime, + Value? endTimeZone, + Value? recurrenceJson, + Value? remindersJson, + Value? attendeesJson, + Value? categoriesJson, + Value? organizerJson, + Value? creatorJson, + Value? colorId, + Value? colorHex, + Value? visibility, + Value? transparencyOrShowAs, + Value? eventType, + Value? webLink, + Value? conferenceJson, + Value? attachmentsJson, + Value? isCancelled, + Value? isDeleted, + Value? rawJson, + Value? createdAtServer, + Value? updatedAtServer, + Value? createdAtLocal, + Value? updatedAtLocal, + Value? syncStatus, + Value? baselineRawJson, + Value? rowid, + }) { + return CalendarEventsCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + calendarSourceId: calendarSourceId ?? this.calendarSourceId, + provider: provider ?? this.provider, + providerCalendarId: providerCalendarId ?? this.providerCalendarId, + providerEventId: providerEventId ?? this.providerEventId, + davCollectionId: davCollectionId ?? this.davCollectionId, + davObjectId: davObjectId ?? this.davObjectId, + davComponentId: davComponentId ?? this.davComponentId, + icalUid: icalUid ?? this.icalUid, + recurrenceIdKey: recurrenceIdKey ?? this.recurrenceIdKey, + occurrenceKey: occurrenceKey ?? this.occurrenceKey, + projectionVersion: projectionVersion ?? this.projectionVersion, + providerRecurringEventId: + providerRecurringEventId ?? this.providerRecurringEventId, + providerOriginalStartKey: + providerOriginalStartKey ?? this.providerOriginalStartKey, + etagOrChangeKey: etagOrChangeKey ?? this.etagOrChangeKey, + status: status ?? this.status, + title: title ?? this.title, + description: description ?? this.description, + location: location ?? this.location, + allDay: allDay ?? this.allDay, + startDate: startDate ?? this.startDate, + startDateTime: startDateTime ?? this.startDateTime, + startTimeZone: startTimeZone ?? this.startTimeZone, + endDate: endDate ?? this.endDate, + endDateTime: endDateTime ?? this.endDateTime, + endTimeZone: endTimeZone ?? this.endTimeZone, + recurrenceJson: recurrenceJson ?? this.recurrenceJson, + remindersJson: remindersJson ?? this.remindersJson, + attendeesJson: attendeesJson ?? this.attendeesJson, + categoriesJson: categoriesJson ?? this.categoriesJson, + organizerJson: organizerJson ?? this.organizerJson, + creatorJson: creatorJson ?? this.creatorJson, + colorId: colorId ?? this.colorId, + colorHex: colorHex ?? this.colorHex, + visibility: visibility ?? this.visibility, + transparencyOrShowAs: transparencyOrShowAs ?? this.transparencyOrShowAs, + eventType: eventType ?? this.eventType, + webLink: webLink ?? this.webLink, + conferenceJson: conferenceJson ?? this.conferenceJson, + attachmentsJson: attachmentsJson ?? this.attachmentsJson, + isCancelled: isCancelled ?? this.isCancelled, + isDeleted: isDeleted ?? this.isDeleted, + rawJson: rawJson ?? this.rawJson, + createdAtServer: createdAtServer ?? this.createdAtServer, + updatedAtServer: updatedAtServer ?? this.updatedAtServer, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + syncStatus: syncStatus ?? this.syncStatus, + baselineRawJson: baselineRawJson ?? this.baselineRawJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (calendarSourceId.present) { + map['calendar_source_id'] = Variable(calendarSourceId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (providerCalendarId.present) { + map['provider_calendar_id'] = Variable(providerCalendarId.value); + } + if (providerEventId.present) { + map['provider_event_id'] = Variable(providerEventId.value); + } + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); + } + if (davObjectId.present) { + map['dav_object_id'] = Variable(davObjectId.value); + } + if (davComponentId.present) { + map['dav_component_id'] = Variable(davComponentId.value); + } + if (icalUid.present) { + map['ical_uid'] = Variable(icalUid.value); + } + if (recurrenceIdKey.present) { + map['recurrence_id_key'] = Variable(recurrenceIdKey.value); + } + if (occurrenceKey.present) { + map['occurrence_key'] = Variable(occurrenceKey.value); + } + if (projectionVersion.present) { + map['projection_version'] = Variable(projectionVersion.value); + } + if (providerRecurringEventId.present) { + map['provider_recurring_event_id'] = Variable( + providerRecurringEventId.value, + ); + } + if (providerOriginalStartKey.present) { + map['provider_original_start_key'] = Variable( + providerOriginalStartKey.value, + ); + } + if (etagOrChangeKey.present) { + map['etag_or_change_key'] = Variable(etagOrChangeKey.value); + } + if (status.present) { + map['status'] = Variable(status.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (location.present) { + map['location'] = Variable(location.value); + } + if (allDay.present) { + map['all_day'] = Variable(allDay.value); + } + if (startDate.present) { + map['start_date'] = Variable(startDate.value); + } + if (startDateTime.present) { + map['start_date_time'] = Variable(startDateTime.value); + } + if (startTimeZone.present) { + map['start_time_zone'] = Variable(startTimeZone.value); + } + if (endDate.present) { + map['end_date'] = Variable(endDate.value); + } + if (endDateTime.present) { + map['end_date_time'] = Variable(endDateTime.value); + } + if (endTimeZone.present) { + map['end_time_zone'] = Variable(endTimeZone.value); + } + if (recurrenceJson.present) { + map['recurrence_json'] = Variable(recurrenceJson.value); + } + if (remindersJson.present) { + map['reminders_json'] = Variable(remindersJson.value); + } + if (attendeesJson.present) { + map['attendees_json'] = Variable(attendeesJson.value); + } + if (categoriesJson.present) { + map['categories_json'] = Variable(categoriesJson.value); + } + if (organizerJson.present) { + map['organizer_json'] = Variable(organizerJson.value); + } + if (creatorJson.present) { + map['creator_json'] = Variable(creatorJson.value); + } + if (colorId.present) { + map['color_id'] = Variable(colorId.value); + } + if (colorHex.present) { + map['color_hex'] = Variable(colorHex.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (transparencyOrShowAs.present) { + map['transparency_or_show_as'] = Variable( + transparencyOrShowAs.value, + ); + } + if (eventType.present) { + map['event_type'] = Variable(eventType.value); + } + if (webLink.present) { + map['web_link'] = Variable(webLink.value); + } + if (conferenceJson.present) { + map['conference_json'] = Variable(conferenceJson.value); + } + if (attachmentsJson.present) { + map['attachments_json'] = Variable(attachmentsJson.value); + } + if (isCancelled.present) { + map['is_cancelled'] = Variable(isCancelled.value); + } + if (isDeleted.present) { + map['is_deleted'] = Variable(isDeleted.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (createdAtServer.present) { + map['created_at_server'] = Variable(createdAtServer.value); + } + if (updatedAtServer.present) { + map['updated_at_server'] = Variable(updatedAtServer.value); + } + if (createdAtLocal.present) { + map['created_at_local'] = Variable(createdAtLocal.value); + } + if (updatedAtLocal.present) { + map['updated_at_local'] = Variable(updatedAtLocal.value); + } + if (syncStatus.present) { + map['sync_status'] = Variable(syncStatus.value); + } + if (baselineRawJson.present) { + map['baseline_raw_json'] = Variable(baselineRawJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CalendarEventsCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('calendarSourceId: $calendarSourceId, ') + ..write('provider: $provider, ') + ..write('providerCalendarId: $providerCalendarId, ') + ..write('providerEventId: $providerEventId, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('davObjectId: $davObjectId, ') + ..write('davComponentId: $davComponentId, ') + ..write('icalUid: $icalUid, ') + ..write('recurrenceIdKey: $recurrenceIdKey, ') + ..write('occurrenceKey: $occurrenceKey, ') + ..write('projectionVersion: $projectionVersion, ') + ..write('providerRecurringEventId: $providerRecurringEventId, ') + ..write('providerOriginalStartKey: $providerOriginalStartKey, ') + ..write('etagOrChangeKey: $etagOrChangeKey, ') + ..write('status: $status, ') + ..write('title: $title, ') + ..write('description: $description, ') + ..write('location: $location, ') + ..write('allDay: $allDay, ') + ..write('startDate: $startDate, ') + ..write('startDateTime: $startDateTime, ') + ..write('startTimeZone: $startTimeZone, ') + ..write('endDate: $endDate, ') + ..write('endDateTime: $endDateTime, ') + ..write('endTimeZone: $endTimeZone, ') + ..write('recurrenceJson: $recurrenceJson, ') + ..write('remindersJson: $remindersJson, ') + ..write('attendeesJson: $attendeesJson, ') + ..write('categoriesJson: $categoriesJson, ') + ..write('organizerJson: $organizerJson, ') + ..write('creatorJson: $creatorJson, ') + ..write('colorId: $colorId, ') + ..write('colorHex: $colorHex, ') + ..write('visibility: $visibility, ') + ..write('transparencyOrShowAs: $transparencyOrShowAs, ') + ..write('eventType: $eventType, ') + ..write('webLink: $webLink, ') + ..write('conferenceJson: $conferenceJson, ') + ..write('attachmentsJson: $attachmentsJson, ') + ..write('isCancelled: $isCancelled, ') + ..write('isDeleted: $isDeleted, ') + ..write('rawJson: $rawJson, ') + ..write('createdAtServer: $createdAtServer, ') + ..write('updatedAtServer: $updatedAtServer, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('syncStatus: $syncStatus, ') + ..write('baselineRawJson: $baselineRawJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CalendarEventAttendeesTable extends CalendarEventAttendees + with TableInfo<$CalendarEventAttendeesTable, CalendarEventAttendee> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CalendarEventAttendeesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _calendarEventIdMeta = const VerificationMeta( + 'calendarEventId', + ); + @override + late final GeneratedColumn calendarEventId = GeneratedColumn( + 'calendar_event_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES calendar_events (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _emailMeta = const VerificationMeta('email'); + @override + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _displayNameMeta = const VerificationMeta( + 'displayName', + ); + @override + late final GeneratedColumn displayName = GeneratedColumn( + 'display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _responseStatusMeta = const VerificationMeta( + 'responseStatus', + ); + @override + late final GeneratedColumn responseStatus = GeneratedColumn( + 'response_status', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _optionalMeta = const VerificationMeta( + 'optional', + ); + @override + late final GeneratedColumn optional = GeneratedColumn( + 'optional', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("optional" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _organizerMeta = const VerificationMeta( + 'organizer', + ); + @override + late final GeneratedColumn organizer = GeneratedColumn( + 'organizer', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("organizer" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _selfMeta = const VerificationMeta('self'); + @override + late final GeneratedColumn self = GeneratedColumn( + 'self', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("self" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', + ); + @override + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + calendarEventId, + email, + displayName, + responseStatus, + optional, + organizer, + self, + rawJson, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'calendar_event_attendees'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('calendar_event_id')) { + context.handle( + _calendarEventIdMeta, + calendarEventId.isAcceptableOrUnknown( + data['calendar_event_id']!, + _calendarEventIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_calendarEventIdMeta); + } + if (data.containsKey('email')) { + context.handle( + _emailMeta, + email.isAcceptableOrUnknown(data['email']!, _emailMeta), + ); + } else if (isInserting) { + context.missing(_emailMeta); + } + if (data.containsKey('display_name')) { + context.handle( + _displayNameMeta, + displayName.isAcceptableOrUnknown( + data['display_name']!, + _displayNameMeta, + ), + ); + } + if (data.containsKey('response_status')) { + context.handle( + _responseStatusMeta, + responseStatus.isAcceptableOrUnknown( + data['response_status']!, + _responseStatusMeta, + ), + ); + } + if (data.containsKey('optional')) { + context.handle( + _optionalMeta, + optional.isAcceptableOrUnknown(data['optional']!, _optionalMeta), + ); + } + if (data.containsKey('organizer')) { + context.handle( + _organizerMeta, + organizer.isAcceptableOrUnknown(data['organizer']!, _organizerMeta), + ); + } + if (data.containsKey('self')) { + context.handle( + _selfMeta, + self.isAcceptableOrUnknown(data['self']!, _selfMeta), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CalendarEventAttendee map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CalendarEventAttendee( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + calendarEventId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}calendar_event_id'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + displayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}display_name'], + ), + responseStatus: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}response_status'], + ), + optional: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}optional'], + )!, + organizer: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}organizer'], + )!, + self: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}self'], + )!, + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + ), + ); + } + + @override + $CalendarEventAttendeesTable createAlias(String alias) { + return $CalendarEventAttendeesTable(attachedDatabase, alias); + } +} + +class CalendarEventAttendee extends DataClass + implements Insertable { + final String id; + final String calendarEventId; + final String email; + final String? displayName; + final String? responseStatus; + final bool optional; + final bool organizer; + final bool self; + final String? rawJson; + const CalendarEventAttendee({ + required this.id, + required this.calendarEventId, + required this.email, + this.displayName, + this.responseStatus, + required this.optional, + required this.organizer, + required this.self, + this.rawJson, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['calendar_event_id'] = Variable(calendarEventId); + map['email'] = Variable(email); + if (!nullToAbsent || displayName != null) { + map['display_name'] = Variable(displayName); + } + if (!nullToAbsent || responseStatus != null) { + map['response_status'] = Variable(responseStatus); + } + map['optional'] = Variable(optional); + map['organizer'] = Variable(organizer); + map['self'] = Variable(self); + if (!nullToAbsent || rawJson != null) { + map['raw_json'] = Variable(rawJson); + } + return map; + } + + CalendarEventAttendeesCompanion toCompanion(bool nullToAbsent) { + return CalendarEventAttendeesCompanion( + id: Value(id), + calendarEventId: Value(calendarEventId), + email: Value(email), + displayName: displayName == null && nullToAbsent + ? const Value.absent() + : Value(displayName), + responseStatus: responseStatus == null && nullToAbsent + ? const Value.absent() + : Value(responseStatus), + optional: Value(optional), + organizer: Value(organizer), + self: Value(self), + rawJson: rawJson == null && nullToAbsent + ? const Value.absent() + : Value(rawJson), + ); + } + + factory CalendarEventAttendee.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CalendarEventAttendee( + id: serializer.fromJson(json['id']), + calendarEventId: serializer.fromJson(json['calendarEventId']), + email: serializer.fromJson(json['email']), + displayName: serializer.fromJson(json['displayName']), + responseStatus: serializer.fromJson(json['responseStatus']), + optional: serializer.fromJson(json['optional']), + organizer: serializer.fromJson(json['organizer']), + self: serializer.fromJson(json['self']), + rawJson: serializer.fromJson(json['rawJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'calendarEventId': serializer.toJson(calendarEventId), + 'email': serializer.toJson(email), + 'displayName': serializer.toJson(displayName), + 'responseStatus': serializer.toJson(responseStatus), + 'optional': serializer.toJson(optional), + 'organizer': serializer.toJson(organizer), + 'self': serializer.toJson(self), + 'rawJson': serializer.toJson(rawJson), + }; + } + + CalendarEventAttendee copyWith({ + String? id, + String? calendarEventId, + String? email, + Value displayName = const Value.absent(), + Value responseStatus = const Value.absent(), + bool? optional, + bool? organizer, + bool? self, + Value rawJson = const Value.absent(), + }) => CalendarEventAttendee( + id: id ?? this.id, + calendarEventId: calendarEventId ?? this.calendarEventId, + email: email ?? this.email, + displayName: displayName.present ? displayName.value : this.displayName, + responseStatus: responseStatus.present + ? responseStatus.value + : this.responseStatus, + optional: optional ?? this.optional, + organizer: organizer ?? this.organizer, + self: self ?? this.self, + rawJson: rawJson.present ? rawJson.value : this.rawJson, + ); + CalendarEventAttendee copyWithCompanion( + CalendarEventAttendeesCompanion data, + ) { + return CalendarEventAttendee( + id: data.id.present ? data.id.value : this.id, + calendarEventId: data.calendarEventId.present + ? data.calendarEventId.value + : this.calendarEventId, + email: data.email.present ? data.email.value : this.email, + displayName: data.displayName.present + ? data.displayName.value + : this.displayName, + responseStatus: data.responseStatus.present + ? data.responseStatus.value + : this.responseStatus, + optional: data.optional.present ? data.optional.value : this.optional, + organizer: data.organizer.present ? data.organizer.value : this.organizer, + self: data.self.present ? data.self.value : this.self, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + ); + } + + @override + String toString() { + return (StringBuffer('CalendarEventAttendee(') + ..write('id: $id, ') + ..write('calendarEventId: $calendarEventId, ') + ..write('email: $email, ') + ..write('displayName: $displayName, ') + ..write('responseStatus: $responseStatus, ') + ..write('optional: $optional, ') + ..write('organizer: $organizer, ') + ..write('self: $self, ') + ..write('rawJson: $rawJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + calendarEventId, + email, + displayName, + responseStatus, + optional, + organizer, + self, + rawJson, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CalendarEventAttendee && + other.id == this.id && + other.calendarEventId == this.calendarEventId && + other.email == this.email && + other.displayName == this.displayName && + other.responseStatus == this.responseStatus && + other.optional == this.optional && + other.organizer == this.organizer && + other.self == this.self && + other.rawJson == this.rawJson); +} + +class CalendarEventAttendeesCompanion + extends UpdateCompanion { + final Value id; + final Value calendarEventId; + final Value email; + final Value displayName; + final Value responseStatus; + final Value optional; + final Value organizer; + final Value self; + final Value rawJson; + final Value rowid; + const CalendarEventAttendeesCompanion({ + this.id = const Value.absent(), + this.calendarEventId = const Value.absent(), + this.email = const Value.absent(), + this.displayName = const Value.absent(), + this.responseStatus = const Value.absent(), + this.optional = const Value.absent(), + this.organizer = const Value.absent(), + this.self = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + CalendarEventAttendeesCompanion.insert({ + required String id, + required String calendarEventId, + required String email, + this.displayName = const Value.absent(), + this.responseStatus = const Value.absent(), + this.optional = const Value.absent(), + this.organizer = const Value.absent(), + this.self = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + calendarEventId = Value(calendarEventId), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? calendarEventId, + Expression? email, + Expression? displayName, + Expression? responseStatus, + Expression? optional, + Expression? organizer, + Expression? self, + Expression? rawJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (calendarEventId != null) 'calendar_event_id': calendarEventId, + if (email != null) 'email': email, + if (displayName != null) 'display_name': displayName, + if (responseStatus != null) 'response_status': responseStatus, + if (optional != null) 'optional': optional, + if (organizer != null) 'organizer': organizer, + if (self != null) 'self': self, + if (rawJson != null) 'raw_json': rawJson, + if (rowid != null) 'rowid': rowid, + }); + } + + CalendarEventAttendeesCompanion copyWith({ + Value? id, + Value? calendarEventId, + Value? email, + Value? displayName, + Value? responseStatus, + Value? optional, + Value? organizer, + Value? self, + Value? rawJson, + Value? rowid, + }) { + return CalendarEventAttendeesCompanion( + id: id ?? this.id, + calendarEventId: calendarEventId ?? this.calendarEventId, + email: email ?? this.email, + displayName: displayName ?? this.displayName, + responseStatus: responseStatus ?? this.responseStatus, + optional: optional ?? this.optional, + organizer: organizer ?? this.organizer, + self: self ?? this.self, + rawJson: rawJson ?? this.rawJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (calendarEventId.present) { + map['calendar_event_id'] = Variable(calendarEventId.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (displayName.present) { + map['display_name'] = Variable(displayName.value); + } + if (responseStatus.present) { + map['response_status'] = Variable(responseStatus.value); + } + if (optional.present) { + map['optional'] = Variable(optional.value); + } + if (organizer.present) { + map['organizer'] = Variable(organizer.value); + } + if (self.present) { + map['self'] = Variable(self.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CalendarEventAttendeesCompanion(') + ..write('id: $id, ') + ..write('calendarEventId: $calendarEventId, ') + ..write('email: $email, ') + ..write('displayName: $displayName, ') + ..write('responseStatus: $responseStatus, ') + ..write('optional: $optional, ') + ..write('organizer: $organizer, ') + ..write('self: $self, ') + ..write('rawJson: $rawJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CalendarEventRemindersTable extends CalendarEventReminders + with TableInfo<$CalendarEventRemindersTable, CalendarEventReminder> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CalendarEventRemindersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _calendarEventIdMeta = const VerificationMeta( + 'calendarEventId', + ); + @override + late final GeneratedColumn calendarEventId = GeneratedColumn( + 'calendar_event_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES calendar_events (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _methodMeta = const VerificationMeta('method'); + @override + late final GeneratedColumn method = GeneratedColumn( + 'method', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _minutesBeforeMeta = const VerificationMeta( + 'minutesBefore', + ); + @override + late final GeneratedColumn minutesBefore = GeneratedColumn( + 'minutes_before', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _absoluteTimeMeta = const VerificationMeta( + 'absoluteTime', + ); + @override + late final GeneratedColumn absoluteTime = GeneratedColumn( + 'absolute_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _enabledMeta = const VerificationMeta( + 'enabled', + ); + @override + late final GeneratedColumn enabled = GeneratedColumn( + 'enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enabled" IN (0, 1))', + ), + defaultValue: const Constant(true), + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', + ); + @override + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + calendarEventId, + provider, + method, + minutesBefore, + absoluteTime, + enabled, + rawJson, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'calendar_event_reminders'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('calendar_event_id')) { + context.handle( + _calendarEventIdMeta, + calendarEventId.isAcceptableOrUnknown( + data['calendar_event_id']!, + _calendarEventIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_calendarEventIdMeta); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('method')) { + context.handle( + _methodMeta, + method.isAcceptableOrUnknown(data['method']!, _methodMeta), + ); + } + if (data.containsKey('minutes_before')) { + context.handle( + _minutesBeforeMeta, + minutesBefore.isAcceptableOrUnknown( + data['minutes_before']!, + _minutesBeforeMeta, + ), + ); + } + if (data.containsKey('absolute_time')) { + context.handle( + _absoluteTimeMeta, + absoluteTime.isAcceptableOrUnknown( + data['absolute_time']!, + _absoluteTimeMeta, + ), + ); + } + if (data.containsKey('enabled')) { + context.handle( + _enabledMeta, + enabled.isAcceptableOrUnknown(data['enabled']!, _enabledMeta), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + CalendarEventReminder map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CalendarEventReminder( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + calendarEventId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}calendar_event_id'], + )!, + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + )!, + method: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}method'], + ), + minutesBefore: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}minutes_before'], + ), + absoluteTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}absolute_time'], + ), + enabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}enabled'], + )!, + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + ), + ); + } + + @override + $CalendarEventRemindersTable createAlias(String alias) { + return $CalendarEventRemindersTable(attachedDatabase, alias); + } +} + +class CalendarEventReminder extends DataClass + implements Insertable { + final String id; + final String calendarEventId; + final String provider; + final String? method; + final int? minutesBefore; + final String? absoluteTime; + final bool enabled; + final String? rawJson; + const CalendarEventReminder({ + required this.id, + required this.calendarEventId, + required this.provider, + this.method, + this.minutesBefore, + this.absoluteTime, + required this.enabled, + this.rawJson, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['calendar_event_id'] = Variable(calendarEventId); + map['provider'] = Variable(provider); + if (!nullToAbsent || method != null) { + map['method'] = Variable(method); + } + if (!nullToAbsent || minutesBefore != null) { + map['minutes_before'] = Variable(minutesBefore); + } + if (!nullToAbsent || absoluteTime != null) { + map['absolute_time'] = Variable(absoluteTime); + } + map['enabled'] = Variable(enabled); + if (!nullToAbsent || rawJson != null) { + map['raw_json'] = Variable(rawJson); + } + return map; + } + + CalendarEventRemindersCompanion toCompanion(bool nullToAbsent) { + return CalendarEventRemindersCompanion( + id: Value(id), + calendarEventId: Value(calendarEventId), + provider: Value(provider), + method: method == null && nullToAbsent + ? const Value.absent() + : Value(method), + minutesBefore: minutesBefore == null && nullToAbsent + ? const Value.absent() + : Value(minutesBefore), + absoluteTime: absoluteTime == null && nullToAbsent + ? const Value.absent() + : Value(absoluteTime), + enabled: Value(enabled), + rawJson: rawJson == null && nullToAbsent + ? const Value.absent() + : Value(rawJson), + ); + } + + factory CalendarEventReminder.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CalendarEventReminder( + id: serializer.fromJson(json['id']), + calendarEventId: serializer.fromJson(json['calendarEventId']), + provider: serializer.fromJson(json['provider']), + method: serializer.fromJson(json['method']), + minutesBefore: serializer.fromJson(json['minutesBefore']), + absoluteTime: serializer.fromJson(json['absoluteTime']), + enabled: serializer.fromJson(json['enabled']), + rawJson: serializer.fromJson(json['rawJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'calendarEventId': serializer.toJson(calendarEventId), + 'provider': serializer.toJson(provider), + 'method': serializer.toJson(method), + 'minutesBefore': serializer.toJson(minutesBefore), + 'absoluteTime': serializer.toJson(absoluteTime), + 'enabled': serializer.toJson(enabled), + 'rawJson': serializer.toJson(rawJson), + }; + } + + CalendarEventReminder copyWith({ + String? id, + String? calendarEventId, + String? provider, + Value method = const Value.absent(), + Value minutesBefore = const Value.absent(), + Value absoluteTime = const Value.absent(), + bool? enabled, + Value rawJson = const Value.absent(), + }) => CalendarEventReminder( + id: id ?? this.id, + calendarEventId: calendarEventId ?? this.calendarEventId, + provider: provider ?? this.provider, + method: method.present ? method.value : this.method, + minutesBefore: minutesBefore.present + ? minutesBefore.value + : this.minutesBefore, + absoluteTime: absoluteTime.present ? absoluteTime.value : this.absoluteTime, + enabled: enabled ?? this.enabled, + rawJson: rawJson.present ? rawJson.value : this.rawJson, + ); + CalendarEventReminder copyWithCompanion( + CalendarEventRemindersCompanion data, + ) { + return CalendarEventReminder( + id: data.id.present ? data.id.value : this.id, + calendarEventId: data.calendarEventId.present + ? data.calendarEventId.value + : this.calendarEventId, + provider: data.provider.present ? data.provider.value : this.provider, + method: data.method.present ? data.method.value : this.method, + minutesBefore: data.minutesBefore.present + ? data.minutesBefore.value + : this.minutesBefore, + absoluteTime: data.absoluteTime.present + ? data.absoluteTime.value + : this.absoluteTime, + enabled: data.enabled.present ? data.enabled.value : this.enabled, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + ); + } + + @override + String toString() { + return (StringBuffer('CalendarEventReminder(') + ..write('id: $id, ') + ..write('calendarEventId: $calendarEventId, ') + ..write('provider: $provider, ') + ..write('method: $method, ') + ..write('minutesBefore: $minutesBefore, ') + ..write('absoluteTime: $absoluteTime, ') + ..write('enabled: $enabled, ') + ..write('rawJson: $rawJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + calendarEventId, + provider, + method, + minutesBefore, + absoluteTime, + enabled, + rawJson, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CalendarEventReminder && + other.id == this.id && + other.calendarEventId == this.calendarEventId && + other.provider == this.provider && + other.method == this.method && + other.minutesBefore == this.minutesBefore && + other.absoluteTime == this.absoluteTime && + other.enabled == this.enabled && + other.rawJson == this.rawJson); +} + +class CalendarEventRemindersCompanion + extends UpdateCompanion { + final Value id; + final Value calendarEventId; + final Value provider; + final Value method; + final Value minutesBefore; + final Value absoluteTime; + final Value enabled; + final Value rawJson; + final Value rowid; + const CalendarEventRemindersCompanion({ + this.id = const Value.absent(), + this.calendarEventId = const Value.absent(), + this.provider = const Value.absent(), + this.method = const Value.absent(), + this.minutesBefore = const Value.absent(), + this.absoluteTime = const Value.absent(), + this.enabled = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + CalendarEventRemindersCompanion.insert({ + required String id, + required String calendarEventId, + required String provider, + this.method = const Value.absent(), + this.minutesBefore = const Value.absent(), + this.absoluteTime = const Value.absent(), + this.enabled = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + calendarEventId = Value(calendarEventId), + provider = Value(provider); + static Insertable custom({ + Expression? id, + Expression? calendarEventId, + Expression? provider, + Expression? method, + Expression? minutesBefore, + Expression? absoluteTime, + Expression? enabled, + Expression? rawJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (calendarEventId != null) 'calendar_event_id': calendarEventId, + if (provider != null) 'provider': provider, + if (method != null) 'method': method, + if (minutesBefore != null) 'minutes_before': minutesBefore, + if (absoluteTime != null) 'absolute_time': absoluteTime, + if (enabled != null) 'enabled': enabled, + if (rawJson != null) 'raw_json': rawJson, + if (rowid != null) 'rowid': rowid, + }); + } + + CalendarEventRemindersCompanion copyWith({ + Value? id, + Value? calendarEventId, + Value? provider, + Value? method, + Value? minutesBefore, + Value? absoluteTime, + Value? enabled, + Value? rawJson, + Value? rowid, + }) { + return CalendarEventRemindersCompanion( + id: id ?? this.id, + calendarEventId: calendarEventId ?? this.calendarEventId, + provider: provider ?? this.provider, + method: method ?? this.method, + minutesBefore: minutesBefore ?? this.minutesBefore, + absoluteTime: absoluteTime ?? this.absoluteTime, + enabled: enabled ?? this.enabled, + rawJson: rawJson ?? this.rawJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (calendarEventId.present) { + map['calendar_event_id'] = Variable(calendarEventId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (method.present) { + map['method'] = Variable(method.value); + } + if (minutesBefore.present) { + map['minutes_before'] = Variable(minutesBefore.value); + } + if (absoluteTime.present) { + map['absolute_time'] = Variable(absoluteTime.value); + } + if (enabled.present) { + map['enabled'] = Variable(enabled.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CalendarEventRemindersCompanion(') + ..write('id: $id, ') + ..write('calendarEventId: $calendarEventId, ') + ..write('provider: $provider, ') + ..write('method: $method, ') + ..write('minutesBefore: $minutesBefore, ') + ..write('absoluteTime: $absoluteTime, ') + ..write('enabled: $enabled, ') + ..write('rawJson: $rawJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $SyncCursorsTable extends SyncCursors + with TableInfo<$SyncCursorsTable, SyncCursor> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SyncCursorsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _projectionSourceIdMeta = + const VerificationMeta('projectionSourceId'); + @override + late final GeneratedColumn projectionSourceId = + GeneratedColumn( + 'projection_source_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES calendar_sources (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _transportMeta = const VerificationMeta( + 'transport', + ); + @override + late final GeneratedColumn transport = GeneratedColumn( + 'transport', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _syncScopeKindMeta = const VerificationMeta( + 'syncScopeKind', + ); + @override + late final GeneratedColumn syncScopeKind = GeneratedColumn( + 'sync_scope_kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _davCollectionIdMeta = const VerificationMeta( + 'davCollectionId', + ); + @override + late final GeneratedColumn davCollectionId = GeneratedColumn( + 'dav_collection_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES dav_collections (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _cursorKindMeta = const VerificationMeta( + 'cursorKind', + ); + @override + late final GeneratedColumn cursorKind = GeneratedColumn( + 'cursor_kind', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _cursorValueMeta = const VerificationMeta( + 'cursorValue', + ); + @override + late final GeneratedColumn cursorValue = GeneratedColumn( + 'cursor_value', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _rangeStartMeta = const VerificationMeta( + 'rangeStart', + ); + @override + late final GeneratedColumn rangeStart = GeneratedColumn( + 'range_start', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _rangeEndMeta = const VerificationMeta( + 'rangeEnd', + ); + @override + late final GeneratedColumn rangeEnd = GeneratedColumn( + 'range_end', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _baselineGenerationMeta = + const VerificationMeta('baselineGeneration'); + @override + late final GeneratedColumn baselineGeneration = GeneratedColumn( + 'baseline_generation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _inProgressCursorMeta = const VerificationMeta( + 'inProgressCursor', + ); + @override + late final GeneratedColumn inProgressCursor = GeneratedColumn( + 'in_progress_cursor', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _inProgressGenerationMeta = + const VerificationMeta('inProgressGeneration'); + @override + late final GeneratedColumn inProgressGeneration = GeneratedColumn( + 'in_progress_generation', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastCompleteSyncAtMeta = + const VerificationMeta('lastCompleteSyncAt'); + @override + late final GeneratedColumn lastCompleteSyncAt = GeneratedColumn( + 'last_complete_sync_at', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastFailureCodeMeta = const VerificationMeta( + 'lastFailureCode', + ); + @override + late final GeneratedColumn lastFailureCode = GeneratedColumn( + 'last_failure_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _stateSchemaVersionMeta = + const VerificationMeta('stateSchemaVersion'); + @override + late final GeneratedColumn stateSchemaVersion = GeneratedColumn( + 'state_schema_version', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(1), + ); + static const VerificationMeta _stateJsonMeta = const VerificationMeta( + 'stateJson', + ); + @override + late final GeneratedColumn stateJson = GeneratedColumn( + 'state_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + accountId, + projectionSourceId, + provider, + transport, + syncScopeKind, + davCollectionId, + cursorKind, + cursorValue, + rangeStart, + rangeEnd, + baselineGeneration, + inProgressCursor, + inProgressGeneration, + lastCompleteSyncAt, + lastFailureCode, + stateSchemaVersion, + stateJson, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'sync_cursors'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('projection_source_id')) { + context.handle( + _projectionSourceIdMeta, + projectionSourceId.isAcceptableOrUnknown( + data['projection_source_id']!, + _projectionSourceIdMeta, + ), + ); + } + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('transport')) { + context.handle( + _transportMeta, + transport.isAcceptableOrUnknown(data['transport']!, _transportMeta), + ); + } else if (isInserting) { + context.missing(_transportMeta); + } + if (data.containsKey('sync_scope_kind')) { + context.handle( + _syncScopeKindMeta, + syncScopeKind.isAcceptableOrUnknown( + data['sync_scope_kind']!, + _syncScopeKindMeta, + ), + ); + } else if (isInserting) { + context.missing(_syncScopeKindMeta); + } + if (data.containsKey('dav_collection_id')) { + context.handle( + _davCollectionIdMeta, + davCollectionId.isAcceptableOrUnknown( + data['dav_collection_id']!, + _davCollectionIdMeta, + ), + ); + } + if (data.containsKey('cursor_kind')) { + context.handle( + _cursorKindMeta, + cursorKind.isAcceptableOrUnknown(data['cursor_kind']!, _cursorKindMeta), + ); + } else if (isInserting) { + context.missing(_cursorKindMeta); + } + if (data.containsKey('cursor_value')) { + context.handle( + _cursorValueMeta, + cursorValue.isAcceptableOrUnknown( + data['cursor_value']!, + _cursorValueMeta, + ), + ); + } else if (isInserting) { + context.missing(_cursorValueMeta); + } + if (data.containsKey('range_start')) { + context.handle( + _rangeStartMeta, + rangeStart.isAcceptableOrUnknown(data['range_start']!, _rangeStartMeta), + ); + } + if (data.containsKey('range_end')) { + context.handle( + _rangeEndMeta, + rangeEnd.isAcceptableOrUnknown(data['range_end']!, _rangeEndMeta), + ); + } + if (data.containsKey('baseline_generation')) { + context.handle( + _baselineGenerationMeta, + baselineGeneration.isAcceptableOrUnknown( + data['baseline_generation']!, + _baselineGenerationMeta, + ), + ); + } + if (data.containsKey('in_progress_cursor')) { + context.handle( + _inProgressCursorMeta, + inProgressCursor.isAcceptableOrUnknown( + data['in_progress_cursor']!, + _inProgressCursorMeta, + ), + ); + } + if (data.containsKey('in_progress_generation')) { + context.handle( + _inProgressGenerationMeta, + inProgressGeneration.isAcceptableOrUnknown( + data['in_progress_generation']!, + _inProgressGenerationMeta, + ), + ); + } + if (data.containsKey('last_complete_sync_at')) { + context.handle( + _lastCompleteSyncAtMeta, + lastCompleteSyncAt.isAcceptableOrUnknown( + data['last_complete_sync_at']!, + _lastCompleteSyncAtMeta, + ), + ); + } + if (data.containsKey('last_failure_code')) { + context.handle( + _lastFailureCodeMeta, + lastFailureCode.isAcceptableOrUnknown( + data['last_failure_code']!, + _lastFailureCodeMeta, + ), + ); + } + if (data.containsKey('state_schema_version')) { + context.handle( + _stateSchemaVersionMeta, + stateSchemaVersion.isAcceptableOrUnknown( + data['state_schema_version']!, + _stateSchemaVersionMeta, + ), + ); + } + if (data.containsKey('state_json')) { + context.handle( + _stateJsonMeta, + stateJson.isAcceptableOrUnknown(data['state_json']!, _stateJsonMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + SyncCursor map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SyncCursor( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + projectionSourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_source_id'], + ), + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + )!, + transport: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}transport'], + )!, + syncScopeKind: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}sync_scope_kind'], + )!, + davCollectionId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dav_collection_id'], + ), + cursorKind: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cursor_kind'], + )!, + cursorValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cursor_value'], + )!, + rangeStart: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}range_start'], + ), + rangeEnd: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}range_end'], + ), + baselineGeneration: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}baseline_generation'], + )!, + inProgressCursor: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}in_progress_cursor'], + ), + inProgressGeneration: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}in_progress_generation'], + ), + lastCompleteSyncAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}last_complete_sync_at'], + ), + lastFailureCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}last_failure_code'], + ), + stateSchemaVersion: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}state_schema_version'], + )!, + stateJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state_json'], + ), + ); + } + + @override + $SyncCursorsTable createAlias(String alias) { + return $SyncCursorsTable(attachedDatabase, alias); + } +} + +class SyncCursor extends DataClass implements Insertable { + final String id; + final String accountId; + final String? projectionSourceId; + final String provider; + final String transport; + final String syncScopeKind; + final String? davCollectionId; + final String cursorKind; + final String cursorValue; + final String? rangeStart; + final String? rangeEnd; + final int baselineGeneration; + final String? inProgressCursor; + final int? inProgressGeneration; + final int? lastCompleteSyncAt; + final String? lastFailureCode; + final int stateSchemaVersion; + final String? stateJson; + const SyncCursor({ + required this.id, + required this.accountId, + this.projectionSourceId, + required this.provider, + required this.transport, + required this.syncScopeKind, + this.davCollectionId, + required this.cursorKind, + required this.cursorValue, + this.rangeStart, + this.rangeEnd, + required this.baselineGeneration, + this.inProgressCursor, + this.inProgressGeneration, + this.lastCompleteSyncAt, + this.lastFailureCode, + required this.stateSchemaVersion, + this.stateJson, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + if (!nullToAbsent || projectionSourceId != null) { + map['projection_source_id'] = Variable(projectionSourceId); + } + map['provider'] = Variable(provider); + map['transport'] = Variable(transport); + map['sync_scope_kind'] = Variable(syncScopeKind); + if (!nullToAbsent || davCollectionId != null) { + map['dav_collection_id'] = Variable(davCollectionId); + } + map['cursor_kind'] = Variable(cursorKind); + map['cursor_value'] = Variable(cursorValue); + if (!nullToAbsent || rangeStart != null) { + map['range_start'] = Variable(rangeStart); + } + if (!nullToAbsent || rangeEnd != null) { + map['range_end'] = Variable(rangeEnd); + } + map['baseline_generation'] = Variable(baselineGeneration); + if (!nullToAbsent || inProgressCursor != null) { + map['in_progress_cursor'] = Variable(inProgressCursor); + } + if (!nullToAbsent || inProgressGeneration != null) { + map['in_progress_generation'] = Variable(inProgressGeneration); + } + if (!nullToAbsent || lastCompleteSyncAt != null) { + map['last_complete_sync_at'] = Variable(lastCompleteSyncAt); + } + if (!nullToAbsent || lastFailureCode != null) { + map['last_failure_code'] = Variable(lastFailureCode); + } + map['state_schema_version'] = Variable(stateSchemaVersion); + if (!nullToAbsent || stateJson != null) { + map['state_json'] = Variable(stateJson); + } + return map; + } + + SyncCursorsCompanion toCompanion(bool nullToAbsent) { + return SyncCursorsCompanion( + id: Value(id), + accountId: Value(accountId), + projectionSourceId: projectionSourceId == null && nullToAbsent + ? const Value.absent() + : Value(projectionSourceId), + provider: Value(provider), + transport: Value(transport), + syncScopeKind: Value(syncScopeKind), + davCollectionId: davCollectionId == null && nullToAbsent + ? const Value.absent() + : Value(davCollectionId), + cursorKind: Value(cursorKind), + cursorValue: Value(cursorValue), + rangeStart: rangeStart == null && nullToAbsent + ? const Value.absent() + : Value(rangeStart), + rangeEnd: rangeEnd == null && nullToAbsent + ? const Value.absent() + : Value(rangeEnd), + baselineGeneration: Value(baselineGeneration), + inProgressCursor: inProgressCursor == null && nullToAbsent + ? const Value.absent() + : Value(inProgressCursor), + inProgressGeneration: inProgressGeneration == null && nullToAbsent + ? const Value.absent() + : Value(inProgressGeneration), + lastCompleteSyncAt: lastCompleteSyncAt == null && nullToAbsent + ? const Value.absent() + : Value(lastCompleteSyncAt), + lastFailureCode: lastFailureCode == null && nullToAbsent + ? const Value.absent() + : Value(lastFailureCode), + stateSchemaVersion: Value(stateSchemaVersion), + stateJson: stateJson == null && nullToAbsent + ? const Value.absent() + : Value(stateJson), + ); + } + + factory SyncCursor.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SyncCursor( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + projectionSourceId: serializer.fromJson( + json['projectionSourceId'], + ), + provider: serializer.fromJson(json['provider']), + transport: serializer.fromJson(json['transport']), + syncScopeKind: serializer.fromJson(json['syncScopeKind']), + davCollectionId: serializer.fromJson(json['davCollectionId']), + cursorKind: serializer.fromJson(json['cursorKind']), + cursorValue: serializer.fromJson(json['cursorValue']), + rangeStart: serializer.fromJson(json['rangeStart']), + rangeEnd: serializer.fromJson(json['rangeEnd']), + baselineGeneration: serializer.fromJson(json['baselineGeneration']), + inProgressCursor: serializer.fromJson(json['inProgressCursor']), + inProgressGeneration: serializer.fromJson( + json['inProgressGeneration'], + ), + lastCompleteSyncAt: serializer.fromJson(json['lastCompleteSyncAt']), + lastFailureCode: serializer.fromJson(json['lastFailureCode']), + stateSchemaVersion: serializer.fromJson(json['stateSchemaVersion']), + stateJson: serializer.fromJson(json['stateJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'projectionSourceId': serializer.toJson(projectionSourceId), + 'provider': serializer.toJson(provider), + 'transport': serializer.toJson(transport), + 'syncScopeKind': serializer.toJson(syncScopeKind), + 'davCollectionId': serializer.toJson(davCollectionId), + 'cursorKind': serializer.toJson(cursorKind), + 'cursorValue': serializer.toJson(cursorValue), + 'rangeStart': serializer.toJson(rangeStart), + 'rangeEnd': serializer.toJson(rangeEnd), + 'baselineGeneration': serializer.toJson(baselineGeneration), + 'inProgressCursor': serializer.toJson(inProgressCursor), + 'inProgressGeneration': serializer.toJson(inProgressGeneration), + 'lastCompleteSyncAt': serializer.toJson(lastCompleteSyncAt), + 'lastFailureCode': serializer.toJson(lastFailureCode), + 'stateSchemaVersion': serializer.toJson(stateSchemaVersion), + 'stateJson': serializer.toJson(stateJson), + }; + } + + SyncCursor copyWith({ + String? id, + String? accountId, + Value projectionSourceId = const Value.absent(), + String? provider, + String? transport, + String? syncScopeKind, + Value davCollectionId = const Value.absent(), + String? cursorKind, + String? cursorValue, + Value rangeStart = const Value.absent(), + Value rangeEnd = const Value.absent(), + int? baselineGeneration, + Value inProgressCursor = const Value.absent(), + Value inProgressGeneration = const Value.absent(), + Value lastCompleteSyncAt = const Value.absent(), + Value lastFailureCode = const Value.absent(), + int? stateSchemaVersion, + Value stateJson = const Value.absent(), + }) => SyncCursor( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + projectionSourceId: projectionSourceId.present + ? projectionSourceId.value + : this.projectionSourceId, + provider: provider ?? this.provider, + transport: transport ?? this.transport, + syncScopeKind: syncScopeKind ?? this.syncScopeKind, + davCollectionId: davCollectionId.present + ? davCollectionId.value + : this.davCollectionId, + cursorKind: cursorKind ?? this.cursorKind, + cursorValue: cursorValue ?? this.cursorValue, + rangeStart: rangeStart.present ? rangeStart.value : this.rangeStart, + rangeEnd: rangeEnd.present ? rangeEnd.value : this.rangeEnd, + baselineGeneration: baselineGeneration ?? this.baselineGeneration, + inProgressCursor: inProgressCursor.present + ? inProgressCursor.value + : this.inProgressCursor, + inProgressGeneration: inProgressGeneration.present + ? inProgressGeneration.value + : this.inProgressGeneration, + lastCompleteSyncAt: lastCompleteSyncAt.present + ? lastCompleteSyncAt.value + : this.lastCompleteSyncAt, + lastFailureCode: lastFailureCode.present + ? lastFailureCode.value + : this.lastFailureCode, + stateSchemaVersion: stateSchemaVersion ?? this.stateSchemaVersion, + stateJson: stateJson.present ? stateJson.value : this.stateJson, + ); + SyncCursor copyWithCompanion(SyncCursorsCompanion data) { + return SyncCursor( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + projectionSourceId: data.projectionSourceId.present + ? data.projectionSourceId.value + : this.projectionSourceId, + provider: data.provider.present ? data.provider.value : this.provider, + transport: data.transport.present ? data.transport.value : this.transport, + syncScopeKind: data.syncScopeKind.present + ? data.syncScopeKind.value + : this.syncScopeKind, + davCollectionId: data.davCollectionId.present + ? data.davCollectionId.value + : this.davCollectionId, + cursorKind: data.cursorKind.present + ? data.cursorKind.value + : this.cursorKind, + cursorValue: data.cursorValue.present + ? data.cursorValue.value + : this.cursorValue, + rangeStart: data.rangeStart.present + ? data.rangeStart.value + : this.rangeStart, + rangeEnd: data.rangeEnd.present ? data.rangeEnd.value : this.rangeEnd, + baselineGeneration: data.baselineGeneration.present + ? data.baselineGeneration.value + : this.baselineGeneration, + inProgressCursor: data.inProgressCursor.present + ? data.inProgressCursor.value + : this.inProgressCursor, + inProgressGeneration: data.inProgressGeneration.present + ? data.inProgressGeneration.value + : this.inProgressGeneration, + lastCompleteSyncAt: data.lastCompleteSyncAt.present + ? data.lastCompleteSyncAt.value + : this.lastCompleteSyncAt, + lastFailureCode: data.lastFailureCode.present + ? data.lastFailureCode.value + : this.lastFailureCode, + stateSchemaVersion: data.stateSchemaVersion.present + ? data.stateSchemaVersion.value + : this.stateSchemaVersion, + stateJson: data.stateJson.present ? data.stateJson.value : this.stateJson, + ); + } + + @override + String toString() { + return (StringBuffer('SyncCursor(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('projectionSourceId: $projectionSourceId, ') + ..write('provider: $provider, ') + ..write('transport: $transport, ') + ..write('syncScopeKind: $syncScopeKind, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('cursorKind: $cursorKind, ') + ..write('cursorValue: $cursorValue, ') + ..write('rangeStart: $rangeStart, ') + ..write('rangeEnd: $rangeEnd, ') + ..write('baselineGeneration: $baselineGeneration, ') + ..write('inProgressCursor: $inProgressCursor, ') + ..write('inProgressGeneration: $inProgressGeneration, ') + ..write('lastCompleteSyncAt: $lastCompleteSyncAt, ') + ..write('lastFailureCode: $lastFailureCode, ') + ..write('stateSchemaVersion: $stateSchemaVersion, ') + ..write('stateJson: $stateJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + accountId, + projectionSourceId, + provider, + transport, + syncScopeKind, + davCollectionId, + cursorKind, + cursorValue, + rangeStart, + rangeEnd, + baselineGeneration, + inProgressCursor, + inProgressGeneration, + lastCompleteSyncAt, + lastFailureCode, + stateSchemaVersion, + stateJson, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SyncCursor && + other.id == this.id && + other.accountId == this.accountId && + other.projectionSourceId == this.projectionSourceId && + other.provider == this.provider && + other.transport == this.transport && + other.syncScopeKind == this.syncScopeKind && + other.davCollectionId == this.davCollectionId && + other.cursorKind == this.cursorKind && + other.cursorValue == this.cursorValue && + other.rangeStart == this.rangeStart && + other.rangeEnd == this.rangeEnd && + other.baselineGeneration == this.baselineGeneration && + other.inProgressCursor == this.inProgressCursor && + other.inProgressGeneration == this.inProgressGeneration && + other.lastCompleteSyncAt == this.lastCompleteSyncAt && + other.lastFailureCode == this.lastFailureCode && + other.stateSchemaVersion == this.stateSchemaVersion && + other.stateJson == this.stateJson); +} + +class SyncCursorsCompanion extends UpdateCompanion { + final Value id; + final Value accountId; + final Value projectionSourceId; + final Value provider; + final Value transport; + final Value syncScopeKind; + final Value davCollectionId; + final Value cursorKind; + final Value cursorValue; + final Value rangeStart; + final Value rangeEnd; + final Value baselineGeneration; + final Value inProgressCursor; + final Value inProgressGeneration; + final Value lastCompleteSyncAt; + final Value lastFailureCode; + final Value stateSchemaVersion; + final Value stateJson; + final Value rowid; + const SyncCursorsCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.projectionSourceId = const Value.absent(), + this.provider = const Value.absent(), + this.transport = const Value.absent(), + this.syncScopeKind = const Value.absent(), + this.davCollectionId = const Value.absent(), + this.cursorKind = const Value.absent(), + this.cursorValue = const Value.absent(), + this.rangeStart = const Value.absent(), + this.rangeEnd = const Value.absent(), + this.baselineGeneration = const Value.absent(), + this.inProgressCursor = const Value.absent(), + this.inProgressGeneration = const Value.absent(), + this.lastCompleteSyncAt = const Value.absent(), + this.lastFailureCode = const Value.absent(), + this.stateSchemaVersion = const Value.absent(), + this.stateJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + SyncCursorsCompanion.insert({ + required String id, + required String accountId, + this.projectionSourceId = const Value.absent(), + required String provider, + required String transport, + required String syncScopeKind, + this.davCollectionId = const Value.absent(), + required String cursorKind, + required String cursorValue, + this.rangeStart = const Value.absent(), + this.rangeEnd = const Value.absent(), + this.baselineGeneration = const Value.absent(), + this.inProgressCursor = const Value.absent(), + this.inProgressGeneration = const Value.absent(), + this.lastCompleteSyncAt = const Value.absent(), + this.lastFailureCode = const Value.absent(), + this.stateSchemaVersion = const Value.absent(), + this.stateJson = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + provider = Value(provider), + transport = Value(transport), + syncScopeKind = Value(syncScopeKind), + cursorKind = Value(cursorKind), + cursorValue = Value(cursorValue); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? projectionSourceId, + Expression? provider, + Expression? transport, + Expression? syncScopeKind, + Expression? davCollectionId, + Expression? cursorKind, + Expression? cursorValue, + Expression? rangeStart, + Expression? rangeEnd, + Expression? baselineGeneration, + Expression? inProgressCursor, + Expression? inProgressGeneration, + Expression? lastCompleteSyncAt, + Expression? lastFailureCode, + Expression? stateSchemaVersion, + Expression? stateJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (projectionSourceId != null) + 'projection_source_id': projectionSourceId, + if (provider != null) 'provider': provider, + if (transport != null) 'transport': transport, + if (syncScopeKind != null) 'sync_scope_kind': syncScopeKind, + if (davCollectionId != null) 'dav_collection_id': davCollectionId, + if (cursorKind != null) 'cursor_kind': cursorKind, + if (cursorValue != null) 'cursor_value': cursorValue, + if (rangeStart != null) 'range_start': rangeStart, + if (rangeEnd != null) 'range_end': rangeEnd, + if (baselineGeneration != null) 'baseline_generation': baselineGeneration, + if (inProgressCursor != null) 'in_progress_cursor': inProgressCursor, + if (inProgressGeneration != null) + 'in_progress_generation': inProgressGeneration, + if (lastCompleteSyncAt != null) + 'last_complete_sync_at': lastCompleteSyncAt, + if (lastFailureCode != null) 'last_failure_code': lastFailureCode, + if (stateSchemaVersion != null) + 'state_schema_version': stateSchemaVersion, + if (stateJson != null) 'state_json': stateJson, + if (rowid != null) 'rowid': rowid, + }); + } + + SyncCursorsCompanion copyWith({ + Value? id, + Value? accountId, + Value? projectionSourceId, + Value? provider, + Value? transport, + Value? syncScopeKind, + Value? davCollectionId, + Value? cursorKind, + Value? cursorValue, + Value? rangeStart, + Value? rangeEnd, + Value? baselineGeneration, + Value? inProgressCursor, + Value? inProgressGeneration, + Value? lastCompleteSyncAt, + Value? lastFailureCode, + Value? stateSchemaVersion, + Value? stateJson, + Value? rowid, + }) { + return SyncCursorsCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + projectionSourceId: projectionSourceId ?? this.projectionSourceId, + provider: provider ?? this.provider, + transport: transport ?? this.transport, + syncScopeKind: syncScopeKind ?? this.syncScopeKind, + davCollectionId: davCollectionId ?? this.davCollectionId, + cursorKind: cursorKind ?? this.cursorKind, + cursorValue: cursorValue ?? this.cursorValue, + rangeStart: rangeStart ?? this.rangeStart, + rangeEnd: rangeEnd ?? this.rangeEnd, + baselineGeneration: baselineGeneration ?? this.baselineGeneration, + inProgressCursor: inProgressCursor ?? this.inProgressCursor, + inProgressGeneration: inProgressGeneration ?? this.inProgressGeneration, + lastCompleteSyncAt: lastCompleteSyncAt ?? this.lastCompleteSyncAt, + lastFailureCode: lastFailureCode ?? this.lastFailureCode, + stateSchemaVersion: stateSchemaVersion ?? this.stateSchemaVersion, + stateJson: stateJson ?? this.stateJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (projectionSourceId.present) { + map['projection_source_id'] = Variable(projectionSourceId.value); + } + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (transport.present) { + map['transport'] = Variable(transport.value); + } + if (syncScopeKind.present) { + map['sync_scope_kind'] = Variable(syncScopeKind.value); + } + if (davCollectionId.present) { + map['dav_collection_id'] = Variable(davCollectionId.value); + } + if (cursorKind.present) { + map['cursor_kind'] = Variable(cursorKind.value); + } + if (cursorValue.present) { + map['cursor_value'] = Variable(cursorValue.value); + } + if (rangeStart.present) { + map['range_start'] = Variable(rangeStart.value); + } + if (rangeEnd.present) { + map['range_end'] = Variable(rangeEnd.value); + } + if (baselineGeneration.present) { + map['baseline_generation'] = Variable(baselineGeneration.value); + } + if (inProgressCursor.present) { + map['in_progress_cursor'] = Variable(inProgressCursor.value); + } + if (inProgressGeneration.present) { + map['in_progress_generation'] = Variable(inProgressGeneration.value); + } + if (lastCompleteSyncAt.present) { + map['last_complete_sync_at'] = Variable(lastCompleteSyncAt.value); + } + if (lastFailureCode.present) { + map['last_failure_code'] = Variable(lastFailureCode.value); + } + if (stateSchemaVersion.present) { + map['state_schema_version'] = Variable(stateSchemaVersion.value); + } + if (stateJson.present) { + map['state_json'] = Variable(stateJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SyncCursorsCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('projectionSourceId: $projectionSourceId, ') + ..write('provider: $provider, ') + ..write('transport: $transport, ') + ..write('syncScopeKind: $syncScopeKind, ') + ..write('davCollectionId: $davCollectionId, ') + ..write('cursorKind: $cursorKind, ') + ..write('cursorValue: $cursorValue, ') + ..write('rangeStart: $rangeStart, ') + ..write('rangeEnd: $rangeEnd, ') + ..write('baselineGeneration: $baselineGeneration, ') + ..write('inProgressCursor: $inProgressCursor, ') + ..write('inProgressGeneration: $inProgressGeneration, ') + ..write('lastCompleteSyncAt: $lastCompleteSyncAt, ') + ..write('lastFailureCode: $lastFailureCode, ') + ..write('stateSchemaVersion: $stateSchemaVersion, ') + ..write('stateJson: $stateJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $CalendarColorsTable extends CalendarColors + with TableInfo<$CalendarColorsTable, CalendarColor> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $CalendarColorsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _providerMeta = const VerificationMeta( + 'provider', + ); + @override + late final GeneratedColumn provider = GeneratedColumn( + 'provider', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _colorTypeMeta = const VerificationMeta( + 'colorType', + ); + @override + late final GeneratedColumn colorType = GeneratedColumn( + 'color_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _colorIdMeta = const VerificationMeta( + 'colorId', + ); + @override + late final GeneratedColumn colorId = GeneratedColumn( + 'color_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _backgroundMeta = const VerificationMeta( + 'background', + ); + @override + late final GeneratedColumn background = GeneratedColumn( + 'background', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _foregroundMeta = const VerificationMeta( + 'foreground', + ); + @override + late final GeneratedColumn foreground = GeneratedColumn( + 'foreground', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _rawJsonMeta = const VerificationMeta( + 'rawJson', + ); + @override + late final GeneratedColumn rawJson = GeneratedColumn( + 'raw_json', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + provider, + colorType, + colorId, + background, + foreground, + rawJson, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'calendar_colors'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('provider')) { + context.handle( + _providerMeta, + provider.isAcceptableOrUnknown(data['provider']!, _providerMeta), + ); + } else if (isInserting) { + context.missing(_providerMeta); + } + if (data.containsKey('color_type')) { + context.handle( + _colorTypeMeta, + colorType.isAcceptableOrUnknown(data['color_type']!, _colorTypeMeta), + ); + } else if (isInserting) { + context.missing(_colorTypeMeta); + } + if (data.containsKey('color_id')) { + context.handle( + _colorIdMeta, + colorId.isAcceptableOrUnknown(data['color_id']!, _colorIdMeta), + ); + } else if (isInserting) { + context.missing(_colorIdMeta); + } + if (data.containsKey('background')) { + context.handle( + _backgroundMeta, + background.isAcceptableOrUnknown(data['background']!, _backgroundMeta), + ); + } else if (isInserting) { + context.missing(_backgroundMeta); + } + if (data.containsKey('foreground')) { + context.handle( + _foregroundMeta, + foreground.isAcceptableOrUnknown(data['foreground']!, _foregroundMeta), + ); + } + if (data.containsKey('raw_json')) { + context.handle( + _rawJsonMeta, + rawJson.isAcceptableOrUnknown(data['raw_json']!, _rawJsonMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {provider, colorType, colorId}; + @override + CalendarColor map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return CalendarColor( + provider: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}provider'], + )!, + colorType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color_type'], + )!, + colorId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color_id'], + )!, + background: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}background'], + )!, + foreground: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}foreground'], + ), + rawJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}raw_json'], + ), + ); + } + + @override + $CalendarColorsTable createAlias(String alias) { + return $CalendarColorsTable(attachedDatabase, alias); + } +} + +class CalendarColor extends DataClass implements Insertable { + final String provider; + final String colorType; + final String colorId; + final String background; + final String? foreground; + final String? rawJson; + const CalendarColor({ + required this.provider, + required this.colorType, + required this.colorId, + required this.background, + this.foreground, + this.rawJson, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['provider'] = Variable(provider); + map['color_type'] = Variable(colorType); + map['color_id'] = Variable(colorId); + map['background'] = Variable(background); + if (!nullToAbsent || foreground != null) { + map['foreground'] = Variable(foreground); + } + if (!nullToAbsent || rawJson != null) { + map['raw_json'] = Variable(rawJson); + } + return map; + } + + CalendarColorsCompanion toCompanion(bool nullToAbsent) { + return CalendarColorsCompanion( + provider: Value(provider), + colorType: Value(colorType), + colorId: Value(colorId), + background: Value(background), + foreground: foreground == null && nullToAbsent + ? const Value.absent() + : Value(foreground), + rawJson: rawJson == null && nullToAbsent + ? const Value.absent() + : Value(rawJson), + ); + } + + factory CalendarColor.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return CalendarColor( + provider: serializer.fromJson(json['provider']), + colorType: serializer.fromJson(json['colorType']), + colorId: serializer.fromJson(json['colorId']), + background: serializer.fromJson(json['background']), + foreground: serializer.fromJson(json['foreground']), + rawJson: serializer.fromJson(json['rawJson']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'provider': serializer.toJson(provider), + 'colorType': serializer.toJson(colorType), + 'colorId': serializer.toJson(colorId), + 'background': serializer.toJson(background), + 'foreground': serializer.toJson(foreground), + 'rawJson': serializer.toJson(rawJson), + }; + } + + CalendarColor copyWith({ + String? provider, + String? colorType, + String? colorId, + String? background, + Value foreground = const Value.absent(), + Value rawJson = const Value.absent(), + }) => CalendarColor( + provider: provider ?? this.provider, + colorType: colorType ?? this.colorType, + colorId: colorId ?? this.colorId, + background: background ?? this.background, + foreground: foreground.present ? foreground.value : this.foreground, + rawJson: rawJson.present ? rawJson.value : this.rawJson, + ); + CalendarColor copyWithCompanion(CalendarColorsCompanion data) { + return CalendarColor( + provider: data.provider.present ? data.provider.value : this.provider, + colorType: data.colorType.present ? data.colorType.value : this.colorType, + colorId: data.colorId.present ? data.colorId.value : this.colorId, + background: data.background.present + ? data.background.value + : this.background, + foreground: data.foreground.present + ? data.foreground.value + : this.foreground, + rawJson: data.rawJson.present ? data.rawJson.value : this.rawJson, + ); + } + + @override + String toString() { + return (StringBuffer('CalendarColor(') + ..write('provider: $provider, ') + ..write('colorType: $colorType, ') + ..write('colorId: $colorId, ') + ..write('background: $background, ') + ..write('foreground: $foreground, ') + ..write('rawJson: $rawJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + provider, + colorType, + colorId, + background, + foreground, + rawJson, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is CalendarColor && + other.provider == this.provider && + other.colorType == this.colorType && + other.colorId == this.colorId && + other.background == this.background && + other.foreground == this.foreground && + other.rawJson == this.rawJson); +} + +class CalendarColorsCompanion extends UpdateCompanion { + final Value provider; + final Value colorType; + final Value colorId; + final Value background; + final Value foreground; + final Value rawJson; + final Value rowid; + const CalendarColorsCompanion({ + this.provider = const Value.absent(), + this.colorType = const Value.absent(), + this.colorId = const Value.absent(), + this.background = const Value.absent(), + this.foreground = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + CalendarColorsCompanion.insert({ + required String provider, + required String colorType, + required String colorId, + required String background, + this.foreground = const Value.absent(), + this.rawJson = const Value.absent(), + this.rowid = const Value.absent(), + }) : provider = Value(provider), + colorType = Value(colorType), + colorId = Value(colorId), + background = Value(background); + static Insertable custom({ + Expression? provider, + Expression? colorType, + Expression? colorId, + Expression? background, + Expression? foreground, + Expression? rawJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (provider != null) 'provider': provider, + if (colorType != null) 'color_type': colorType, + if (colorId != null) 'color_id': colorId, + if (background != null) 'background': background, + if (foreground != null) 'foreground': foreground, + if (rawJson != null) 'raw_json': rawJson, + if (rowid != null) 'rowid': rowid, + }); + } + + CalendarColorsCompanion copyWith({ + Value? provider, + Value? colorType, + Value? colorId, + Value? background, + Value? foreground, + Value? rawJson, + Value? rowid, + }) { + return CalendarColorsCompanion( + provider: provider ?? this.provider, + colorType: colorType ?? this.colorType, + colorId: colorId ?? this.colorId, + background: background ?? this.background, + foreground: foreground ?? this.foreground, + rawJson: rawJson ?? this.rawJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (provider.present) { + map['provider'] = Variable(provider.value); + } + if (colorType.present) { + map['color_type'] = Variable(colorType.value); + } + if (colorId.present) { + map['color_id'] = Variable(colorId.value); + } + if (background.present) { + map['background'] = Variable(background.value); + } + if (foreground.present) { + map['foreground'] = Variable(foreground.value); + } + if (rawJson.present) { + map['raw_json'] = Variable(rawJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CalendarColorsCompanion(') + ..write('provider: $provider, ') + ..write('colorType: $colorType, ') + ..write('colorId: $colorId, ') + ..write('background: $background, ') + ..write('foreground: $foreground, ') + ..write('rawJson: $rawJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ScheduleItemOverridesTable extends ScheduleItemOverrides + with TableInfo<$ScheduleItemOverridesTable, ScheduleItemOverride> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ScheduleItemOverridesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _sourceTypeMeta = const VerificationMeta( + 'sourceType', + ); + @override + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _sourceIdMeta = const VerificationMeta( + 'sourceId', + ); + @override + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _overrideJsonMeta = const VerificationMeta( + 'overrideJson', + ); + @override + late final GeneratedColumn overrideJson = GeneratedColumn( + 'override_json', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( + 'createdAtLocal', + ); + @override + late final GeneratedColumn createdAtLocal = GeneratedColumn( + 'created_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( + 'updatedAtLocal', + ); + @override + late final GeneratedColumn updatedAtLocal = GeneratedColumn( + 'updated_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + accountId, + sourceType, + sourceId, + overrideJson, + createdAtLocal, + updatedAtLocal, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'schedule_item_overrides'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('source_type')) { + context.handle( + _sourceTypeMeta, + sourceType.isAcceptableOrUnknown(data['source_type']!, _sourceTypeMeta), + ); + } else if (isInserting) { + context.missing(_sourceTypeMeta); + } + if (data.containsKey('source_id')) { + context.handle( + _sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta), + ); + } else if (isInserting) { + context.missing(_sourceIdMeta); + } + if (data.containsKey('override_json')) { + context.handle( + _overrideJsonMeta, + overrideJson.isAcceptableOrUnknown( + data['override_json']!, + _overrideJsonMeta, + ), + ); + } else if (isInserting) { + context.missing(_overrideJsonMeta); + } + if (data.containsKey('created_at_local')) { + context.handle( + _createdAtLocalMeta, + createdAtLocal.isAcceptableOrUnknown( + data['created_at_local']!, + _createdAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdAtLocalMeta); + } + if (data.containsKey('updated_at_local')) { + context.handle( + _updatedAtLocalMeta, + updatedAtLocal.isAcceptableOrUnknown( + data['updated_at_local']!, + _updatedAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedAtLocalMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ScheduleItemOverride map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScheduleItemOverride( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + sourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_id'], + )!, + overrideJson: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}override_json'], + )!, + createdAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at_local'], + )!, + updatedAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at_local'], + )!, + ); + } + + @override + $ScheduleItemOverridesTable createAlias(String alias) { + return $ScheduleItemOverridesTable(attachedDatabase, alias); + } +} + +class ScheduleItemOverride extends DataClass + implements Insertable { + final String id; + final String accountId; + final String sourceType; + final String sourceId; + final String overrideJson; + final int createdAtLocal; + final int updatedAtLocal; + const ScheduleItemOverride({ + required this.id, + required this.accountId, + required this.sourceType, + required this.sourceId, + required this.overrideJson, + required this.createdAtLocal, + required this.updatedAtLocal, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + map['source_type'] = Variable(sourceType); + map['source_id'] = Variable(sourceId); + map['override_json'] = Variable(overrideJson); + map['created_at_local'] = Variable(createdAtLocal); + map['updated_at_local'] = Variable(updatedAtLocal); + return map; + } + + ScheduleItemOverridesCompanion toCompanion(bool nullToAbsent) { + return ScheduleItemOverridesCompanion( + id: Value(id), + accountId: Value(accountId), + sourceType: Value(sourceType), + sourceId: Value(sourceId), + overrideJson: Value(overrideJson), + createdAtLocal: Value(createdAtLocal), + updatedAtLocal: Value(updatedAtLocal), + ); + } + + factory ScheduleItemOverride.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScheduleItemOverride( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + sourceType: serializer.fromJson(json['sourceType']), + sourceId: serializer.fromJson(json['sourceId']), + overrideJson: serializer.fromJson(json['overrideJson']), + createdAtLocal: serializer.fromJson(json['createdAtLocal']), + updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'sourceType': serializer.toJson(sourceType), + 'sourceId': serializer.toJson(sourceId), + 'overrideJson': serializer.toJson(overrideJson), + 'createdAtLocal': serializer.toJson(createdAtLocal), + 'updatedAtLocal': serializer.toJson(updatedAtLocal), + }; + } + + ScheduleItemOverride copyWith({ + String? id, + String? accountId, + String? sourceType, + String? sourceId, + String? overrideJson, + int? createdAtLocal, + int? updatedAtLocal, + }) => ScheduleItemOverride( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + sourceType: sourceType ?? this.sourceType, + sourceId: sourceId ?? this.sourceId, + overrideJson: overrideJson ?? this.overrideJson, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + ); + ScheduleItemOverride copyWithCompanion(ScheduleItemOverridesCompanion data) { + return ScheduleItemOverride( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + overrideJson: data.overrideJson.present + ? data.overrideJson.value + : this.overrideJson, + createdAtLocal: data.createdAtLocal.present + ? data.createdAtLocal.value + : this.createdAtLocal, + updatedAtLocal: data.updatedAtLocal.present + ? data.updatedAtLocal.value + : this.updatedAtLocal, + ); + } + + @override + String toString() { + return (StringBuffer('ScheduleItemOverride(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('sourceType: $sourceType, ') + ..write('sourceId: $sourceId, ') + ..write('overrideJson: $overrideJson, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + accountId, + sourceType, + sourceId, + overrideJson, + createdAtLocal, + updatedAtLocal, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScheduleItemOverride && + other.id == this.id && + other.accountId == this.accountId && + other.sourceType == this.sourceType && + other.sourceId == this.sourceId && + other.overrideJson == this.overrideJson && + other.createdAtLocal == this.createdAtLocal && + other.updatedAtLocal == this.updatedAtLocal); +} + +class ScheduleItemOverridesCompanion + extends UpdateCompanion { + final Value id; + final Value accountId; + final Value sourceType; + final Value sourceId; + final Value overrideJson; + final Value createdAtLocal; + final Value updatedAtLocal; + final Value rowid; + const ScheduleItemOverridesCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.sourceType = const Value.absent(), + this.sourceId = const Value.absent(), + this.overrideJson = const Value.absent(), + this.createdAtLocal = const Value.absent(), + this.updatedAtLocal = const Value.absent(), + this.rowid = const Value.absent(), + }); + ScheduleItemOverridesCompanion.insert({ + required String id, + required String accountId, + required String sourceType, + required String sourceId, + required String overrideJson, + required int createdAtLocal, + required int updatedAtLocal, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + sourceType = Value(sourceType), + sourceId = Value(sourceId), + overrideJson = Value(overrideJson), + createdAtLocal = Value(createdAtLocal), + updatedAtLocal = Value(updatedAtLocal); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? sourceType, + Expression? sourceId, + Expression? overrideJson, + Expression? createdAtLocal, + Expression? updatedAtLocal, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (sourceType != null) 'source_type': sourceType, + if (sourceId != null) 'source_id': sourceId, + if (overrideJson != null) 'override_json': overrideJson, + if (createdAtLocal != null) 'created_at_local': createdAtLocal, + if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, + if (rowid != null) 'rowid': rowid, + }); + } + + ScheduleItemOverridesCompanion copyWith({ + Value? id, + Value? accountId, + Value? sourceType, + Value? sourceId, + Value? overrideJson, + Value? createdAtLocal, + Value? updatedAtLocal, + Value? rowid, + }) { + return ScheduleItemOverridesCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + sourceType: sourceType ?? this.sourceType, + sourceId: sourceId ?? this.sourceId, + overrideJson: overrideJson ?? this.overrideJson, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (overrideJson.present) { + map['override_json'] = Variable(overrideJson.value); + } + if (createdAtLocal.present) { + map['created_at_local'] = Variable(createdAtLocal.value); + } + if (updatedAtLocal.present) { + map['updated_at_local'] = Variable(updatedAtLocal.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScheduleItemOverridesCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('sourceType: $sourceType, ') + ..write('sourceId: $sourceId, ') + ..write('overrideJson: $overrideJson, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $NotificationScheduleTable extends NotificationSchedule + with TableInfo<$NotificationScheduleTable, NotificationScheduleData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $NotificationScheduleTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES accounts (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _sourceTypeMeta = const VerificationMeta( + 'sourceType', + ); + @override + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _sourceIdMeta = const VerificationMeta( + 'sourceId', + ); + @override + late final GeneratedColumn sourceId = GeneratedColumn( + 'source_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _scheduledAtUtcMeta = const VerificationMeta( + 'scheduledAtUtc', + ); + @override + late final GeneratedColumn scheduledAtUtc = GeneratedColumn( + 'scheduled_at_utc', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _titleMeta = const VerificationMeta('title'); + @override + late final GeneratedColumn title = GeneratedColumn( + 'title', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _bodyMeta = const VerificationMeta('body'); + @override + late final GeneratedColumn body = GeneratedColumn( + 'body', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _sentAtUtcMeta = const VerificationMeta( + 'sentAtUtc', + ); + @override + late final GeneratedColumn sentAtUtc = GeneratedColumn( + 'sent_at_utc', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _dismissedAtUtcMeta = const VerificationMeta( + 'dismissedAtUtc', + ); + @override + late final GeneratedColumn dismissedAtUtc = GeneratedColumn( + 'dismissed_at_utc', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _snoozedUntilUtcMeta = const VerificationMeta( + 'snoozedUntilUtc', + ); + @override + late final GeneratedColumn snoozedUntilUtc = GeneratedColumn( + 'snoozed_until_utc', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtLocalMeta = const VerificationMeta( + 'createdAtLocal', + ); + @override + late final GeneratedColumn createdAtLocal = GeneratedColumn( + 'created_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtLocalMeta = const VerificationMeta( + 'updatedAtLocal', + ); + @override + late final GeneratedColumn updatedAtLocal = GeneratedColumn( + 'updated_at_local', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + accountId, + sourceType, + sourceId, + scheduledAtUtc, + title, + body, + sentAtUtc, + dismissedAtUtc, + snoozedUntilUtc, + createdAtLocal, + updatedAtLocal, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'notification_schedule'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } else if (isInserting) { + context.missing(_accountIdMeta); + } + if (data.containsKey('source_type')) { + context.handle( + _sourceTypeMeta, + sourceType.isAcceptableOrUnknown(data['source_type']!, _sourceTypeMeta), + ); + } else if (isInserting) { + context.missing(_sourceTypeMeta); + } + if (data.containsKey('source_id')) { + context.handle( + _sourceIdMeta, + sourceId.isAcceptableOrUnknown(data['source_id']!, _sourceIdMeta), + ); + } else if (isInserting) { + context.missing(_sourceIdMeta); + } + if (data.containsKey('scheduled_at_utc')) { + context.handle( + _scheduledAtUtcMeta, + scheduledAtUtc.isAcceptableOrUnknown( + data['scheduled_at_utc']!, + _scheduledAtUtcMeta, + ), + ); + } else if (isInserting) { + context.missing(_scheduledAtUtcMeta); + } + if (data.containsKey('title')) { + context.handle( + _titleMeta, + title.isAcceptableOrUnknown(data['title']!, _titleMeta), + ); + } else if (isInserting) { + context.missing(_titleMeta); + } + if (data.containsKey('body')) { + context.handle( + _bodyMeta, + body.isAcceptableOrUnknown(data['body']!, _bodyMeta), + ); + } + if (data.containsKey('sent_at_utc')) { + context.handle( + _sentAtUtcMeta, + sentAtUtc.isAcceptableOrUnknown(data['sent_at_utc']!, _sentAtUtcMeta), + ); + } + if (data.containsKey('dismissed_at_utc')) { + context.handle( + _dismissedAtUtcMeta, + dismissedAtUtc.isAcceptableOrUnknown( + data['dismissed_at_utc']!, + _dismissedAtUtcMeta, + ), + ); + } + if (data.containsKey('snoozed_until_utc')) { + context.handle( + _snoozedUntilUtcMeta, + snoozedUntilUtc.isAcceptableOrUnknown( + data['snoozed_until_utc']!, + _snoozedUntilUtcMeta, + ), + ); + } + if (data.containsKey('created_at_local')) { + context.handle( + _createdAtLocalMeta, + createdAtLocal.isAcceptableOrUnknown( + data['created_at_local']!, + _createdAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_createdAtLocalMeta); + } + if (data.containsKey('updated_at_local')) { + context.handle( + _updatedAtLocalMeta, + updatedAtLocal.isAcceptableOrUnknown( + data['updated_at_local']!, + _updatedAtLocalMeta, + ), + ); + } else if (isInserting) { + context.missing(_updatedAtLocalMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + NotificationScheduleData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return NotificationScheduleData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + sourceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_id'], + )!, + scheduledAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}scheduled_at_utc'], + )!, + title: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}title'], + )!, + body: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}body'], + ), + sentAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sent_at_utc'], + ), + dismissedAtUtc: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}dismissed_at_utc'], + ), + snoozedUntilUtc: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}snoozed_until_utc'], + ), + createdAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at_local'], + )!, + updatedAtLocal: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}updated_at_local'], + )!, + ); + } + + @override + $NotificationScheduleTable createAlias(String alias) { + return $NotificationScheduleTable(attachedDatabase, alias); + } +} + +class NotificationScheduleData extends DataClass + implements Insertable { + final String id; + final String accountId; + final String sourceType; + final String sourceId; + final int scheduledAtUtc; + final String title; + final String? body; + final int? sentAtUtc; + final int? dismissedAtUtc; + final int? snoozedUntilUtc; + final int createdAtLocal; + final int updatedAtLocal; + const NotificationScheduleData({ + required this.id, + required this.accountId, + required this.sourceType, + required this.sourceId, + required this.scheduledAtUtc, + required this.title, + this.body, + this.sentAtUtc, + this.dismissedAtUtc, + this.snoozedUntilUtc, + required this.createdAtLocal, + required this.updatedAtLocal, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['account_id'] = Variable(accountId); + map['source_type'] = Variable(sourceType); + map['source_id'] = Variable(sourceId); + map['scheduled_at_utc'] = Variable(scheduledAtUtc); + map['title'] = Variable(title); + if (!nullToAbsent || body != null) { + map['body'] = Variable(body); + } + if (!nullToAbsent || sentAtUtc != null) { + map['sent_at_utc'] = Variable(sentAtUtc); + } + if (!nullToAbsent || dismissedAtUtc != null) { + map['dismissed_at_utc'] = Variable(dismissedAtUtc); + } + if (!nullToAbsent || snoozedUntilUtc != null) { + map['snoozed_until_utc'] = Variable(snoozedUntilUtc); + } + map['created_at_local'] = Variable(createdAtLocal); + map['updated_at_local'] = Variable(updatedAtLocal); + return map; + } + + NotificationScheduleCompanion toCompanion(bool nullToAbsent) { + return NotificationScheduleCompanion( + id: Value(id), + accountId: Value(accountId), + sourceType: Value(sourceType), + sourceId: Value(sourceId), + scheduledAtUtc: Value(scheduledAtUtc), + title: Value(title), + body: body == null && nullToAbsent ? const Value.absent() : Value(body), + sentAtUtc: sentAtUtc == null && nullToAbsent + ? const Value.absent() + : Value(sentAtUtc), + dismissedAtUtc: dismissedAtUtc == null && nullToAbsent + ? const Value.absent() + : Value(dismissedAtUtc), + snoozedUntilUtc: snoozedUntilUtc == null && nullToAbsent + ? const Value.absent() + : Value(snoozedUntilUtc), + createdAtLocal: Value(createdAtLocal), + updatedAtLocal: Value(updatedAtLocal), + ); + } + + factory NotificationScheduleData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return NotificationScheduleData( + id: serializer.fromJson(json['id']), + accountId: serializer.fromJson(json['accountId']), + sourceType: serializer.fromJson(json['sourceType']), + sourceId: serializer.fromJson(json['sourceId']), + scheduledAtUtc: serializer.fromJson(json['scheduledAtUtc']), + title: serializer.fromJson(json['title']), + body: serializer.fromJson(json['body']), + sentAtUtc: serializer.fromJson(json['sentAtUtc']), + dismissedAtUtc: serializer.fromJson(json['dismissedAtUtc']), + snoozedUntilUtc: serializer.fromJson(json['snoozedUntilUtc']), + createdAtLocal: serializer.fromJson(json['createdAtLocal']), + updatedAtLocal: serializer.fromJson(json['updatedAtLocal']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'accountId': serializer.toJson(accountId), + 'sourceType': serializer.toJson(sourceType), + 'sourceId': serializer.toJson(sourceId), + 'scheduledAtUtc': serializer.toJson(scheduledAtUtc), + 'title': serializer.toJson(title), + 'body': serializer.toJson(body), + 'sentAtUtc': serializer.toJson(sentAtUtc), + 'dismissedAtUtc': serializer.toJson(dismissedAtUtc), + 'snoozedUntilUtc': serializer.toJson(snoozedUntilUtc), + 'createdAtLocal': serializer.toJson(createdAtLocal), + 'updatedAtLocal': serializer.toJson(updatedAtLocal), + }; + } + + NotificationScheduleData copyWith({ + String? id, + String? accountId, + String? sourceType, + String? sourceId, + int? scheduledAtUtc, + String? title, + Value body = const Value.absent(), + Value sentAtUtc = const Value.absent(), + Value dismissedAtUtc = const Value.absent(), + Value snoozedUntilUtc = const Value.absent(), + int? createdAtLocal, + int? updatedAtLocal, + }) => NotificationScheduleData( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + sourceType: sourceType ?? this.sourceType, + sourceId: sourceId ?? this.sourceId, + scheduledAtUtc: scheduledAtUtc ?? this.scheduledAtUtc, + title: title ?? this.title, + body: body.present ? body.value : this.body, + sentAtUtc: sentAtUtc.present ? sentAtUtc.value : this.sentAtUtc, + dismissedAtUtc: dismissedAtUtc.present + ? dismissedAtUtc.value + : this.dismissedAtUtc, + snoozedUntilUtc: snoozedUntilUtc.present + ? snoozedUntilUtc.value + : this.snoozedUntilUtc, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + ); + NotificationScheduleData copyWithCompanion( + NotificationScheduleCompanion data, + ) { + return NotificationScheduleData( + id: data.id.present ? data.id.value : this.id, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + sourceId: data.sourceId.present ? data.sourceId.value : this.sourceId, + scheduledAtUtc: data.scheduledAtUtc.present + ? data.scheduledAtUtc.value + : this.scheduledAtUtc, + title: data.title.present ? data.title.value : this.title, + body: data.body.present ? data.body.value : this.body, + sentAtUtc: data.sentAtUtc.present ? data.sentAtUtc.value : this.sentAtUtc, + dismissedAtUtc: data.dismissedAtUtc.present + ? data.dismissedAtUtc.value + : this.dismissedAtUtc, + snoozedUntilUtc: data.snoozedUntilUtc.present + ? data.snoozedUntilUtc.value + : this.snoozedUntilUtc, + createdAtLocal: data.createdAtLocal.present + ? data.createdAtLocal.value + : this.createdAtLocal, + updatedAtLocal: data.updatedAtLocal.present + ? data.updatedAtLocal.value + : this.updatedAtLocal, + ); + } + + @override + String toString() { + return (StringBuffer('NotificationScheduleData(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('sourceType: $sourceType, ') + ..write('sourceId: $sourceId, ') + ..write('scheduledAtUtc: $scheduledAtUtc, ') + ..write('title: $title, ') + ..write('body: $body, ') + ..write('sentAtUtc: $sentAtUtc, ') + ..write('dismissedAtUtc: $dismissedAtUtc, ') + ..write('snoozedUntilUtc: $snoozedUntilUtc, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + accountId, + sourceType, + sourceId, + scheduledAtUtc, + title, + body, + sentAtUtc, + dismissedAtUtc, + snoozedUntilUtc, + createdAtLocal, + updatedAtLocal, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is NotificationScheduleData && + other.id == this.id && + other.accountId == this.accountId && + other.sourceType == this.sourceType && + other.sourceId == this.sourceId && + other.scheduledAtUtc == this.scheduledAtUtc && + other.title == this.title && + other.body == this.body && + other.sentAtUtc == this.sentAtUtc && + other.dismissedAtUtc == this.dismissedAtUtc && + other.snoozedUntilUtc == this.snoozedUntilUtc && + other.createdAtLocal == this.createdAtLocal && + other.updatedAtLocal == this.updatedAtLocal); +} + +class NotificationScheduleCompanion + extends UpdateCompanion { + final Value id; + final Value accountId; + final Value sourceType; + final Value sourceId; + final Value scheduledAtUtc; + final Value title; + final Value body; + final Value sentAtUtc; + final Value dismissedAtUtc; + final Value snoozedUntilUtc; + final Value createdAtLocal; + final Value updatedAtLocal; + final Value rowid; + const NotificationScheduleCompanion({ + this.id = const Value.absent(), + this.accountId = const Value.absent(), + this.sourceType = const Value.absent(), + this.sourceId = const Value.absent(), + this.scheduledAtUtc = const Value.absent(), + this.title = const Value.absent(), + this.body = const Value.absent(), + this.sentAtUtc = const Value.absent(), + this.dismissedAtUtc = const Value.absent(), + this.snoozedUntilUtc = const Value.absent(), + this.createdAtLocal = const Value.absent(), + this.updatedAtLocal = const Value.absent(), + this.rowid = const Value.absent(), + }); + NotificationScheduleCompanion.insert({ + required String id, + required String accountId, + required String sourceType, + required String sourceId, + required int scheduledAtUtc, + required String title, + this.body = const Value.absent(), + this.sentAtUtc = const Value.absent(), + this.dismissedAtUtc = const Value.absent(), + this.snoozedUntilUtc = const Value.absent(), + required int createdAtLocal, + required int updatedAtLocal, + this.rowid = const Value.absent(), + }) : id = Value(id), + accountId = Value(accountId), + sourceType = Value(sourceType), + sourceId = Value(sourceId), + scheduledAtUtc = Value(scheduledAtUtc), + title = Value(title), + createdAtLocal = Value(createdAtLocal), + updatedAtLocal = Value(updatedAtLocal); + static Insertable custom({ + Expression? id, + Expression? accountId, + Expression? sourceType, + Expression? sourceId, + Expression? scheduledAtUtc, + Expression? title, + Expression? body, + Expression? sentAtUtc, + Expression? dismissedAtUtc, + Expression? snoozedUntilUtc, + Expression? createdAtLocal, + Expression? updatedAtLocal, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (accountId != null) 'account_id': accountId, + if (sourceType != null) 'source_type': sourceType, + if (sourceId != null) 'source_id': sourceId, + if (scheduledAtUtc != null) 'scheduled_at_utc': scheduledAtUtc, + if (title != null) 'title': title, + if (body != null) 'body': body, + if (sentAtUtc != null) 'sent_at_utc': sentAtUtc, + if (dismissedAtUtc != null) 'dismissed_at_utc': dismissedAtUtc, + if (snoozedUntilUtc != null) 'snoozed_until_utc': snoozedUntilUtc, + if (createdAtLocal != null) 'created_at_local': createdAtLocal, + if (updatedAtLocal != null) 'updated_at_local': updatedAtLocal, + if (rowid != null) 'rowid': rowid, + }); + } + + NotificationScheduleCompanion copyWith({ + Value? id, + Value? accountId, + Value? sourceType, + Value? sourceId, + Value? scheduledAtUtc, + Value? title, + Value? body, + Value? sentAtUtc, + Value? dismissedAtUtc, + Value? snoozedUntilUtc, + Value? createdAtLocal, + Value? updatedAtLocal, + Value? rowid, + }) { + return NotificationScheduleCompanion( + id: id ?? this.id, + accountId: accountId ?? this.accountId, + sourceType: sourceType ?? this.sourceType, + sourceId: sourceId ?? this.sourceId, + scheduledAtUtc: scheduledAtUtc ?? this.scheduledAtUtc, + title: title ?? this.title, + body: body ?? this.body, + sentAtUtc: sentAtUtc ?? this.sentAtUtc, + dismissedAtUtc: dismissedAtUtc ?? this.dismissedAtUtc, + snoozedUntilUtc: snoozedUntilUtc ?? this.snoozedUntilUtc, + createdAtLocal: createdAtLocal ?? this.createdAtLocal, + updatedAtLocal: updatedAtLocal ?? this.updatedAtLocal, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + if (sourceId.present) { + map['source_id'] = Variable(sourceId.value); + } + if (scheduledAtUtc.present) { + map['scheduled_at_utc'] = Variable(scheduledAtUtc.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (body.present) { + map['body'] = Variable(body.value); + } + if (sentAtUtc.present) { + map['sent_at_utc'] = Variable(sentAtUtc.value); + } + if (dismissedAtUtc.present) { + map['dismissed_at_utc'] = Variable(dismissedAtUtc.value); + } + if (snoozedUntilUtc.present) { + map['snoozed_until_utc'] = Variable(snoozedUntilUtc.value); + } + if (createdAtLocal.present) { + map['created_at_local'] = Variable(createdAtLocal.value); + } + if (updatedAtLocal.present) { + map['updated_at_local'] = Variable(updatedAtLocal.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('NotificationScheduleCompanion(') + ..write('id: $id, ') + ..write('accountId: $accountId, ') + ..write('sourceType: $sourceType, ') + ..write('sourceId: $sourceId, ') + ..write('scheduledAtUtc: $scheduledAtUtc, ') + ..write('title: $title, ') + ..write('body: $body, ') + ..write('sentAtUtc: $sentAtUtc, ') + ..write('dismissedAtUtc: $dismissedAtUtc, ') + ..write('snoozedUntilUtc: $snoozedUntilUtc, ') + ..write('createdAtLocal: $createdAtLocal, ') + ..write('updatedAtLocal: $updatedAtLocal, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $AccountsTable accounts = $AccountsTable(this); + late final $DavAccountServicesTable davAccountServices = + $DavAccountServicesTable(this); + late final $DavCollectionsTable davCollections = $DavCollectionsTable(this); + late final $DavObjectsTable davObjects = $DavObjectsTable(this); + late final $DavObjectComponentsTable davObjectComponents = + $DavObjectComponentsTable(this); + late final $DavConflictSnapshotsTable davConflictSnapshots = + $DavConflictSnapshotsTable(this); + late final $TaskListsTable taskLists = $TaskListsTable(this); + late final $TasksTable tasks = $TasksTable(this); + late final $PendingOpsTable pendingOps = $PendingOpsTable(this); + late final $SyncRunsTable syncRuns = $SyncRunsTable(this); + late final $CalendarSourcesTable calendarSources = $CalendarSourcesTable( + this, + ); + late final $CalendarEventsTable calendarEvents = $CalendarEventsTable(this); + late final $CalendarEventAttendeesTable calendarEventAttendees = + $CalendarEventAttendeesTable(this); + late final $CalendarEventRemindersTable calendarEventReminders = + $CalendarEventRemindersTable(this); + late final $SyncCursorsTable syncCursors = $SyncCursorsTable(this); + late final $CalendarColorsTable calendarColors = $CalendarColorsTable(this); + late final $ScheduleItemOverridesTable scheduleItemOverrides = + $ScheduleItemOverridesTable(this); + late final $NotificationScheduleTable notificationSchedule = + $NotificationScheduleTable(this); + late final TaskListsDao taskListsDao = TaskListsDao(this as AppDatabase); + late final TasksDao tasksDao = TasksDao(this as AppDatabase); + late final PendingOpsDao pendingOpsDao = PendingOpsDao(this as AppDatabase); + late final SyncRunsDao syncRunsDao = SyncRunsDao(this as AppDatabase); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + accounts, + davAccountServices, + davCollections, + davObjects, + davObjectComponents, + davConflictSnapshots, + taskLists, + tasks, + pendingOps, + syncRuns, + calendarSources, + calendarEvents, + calendarEventAttendees, + calendarEventReminders, + syncCursors, + calendarColors, + scheduleItemOverrides, + notificationSchedule, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_account_services', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_collections', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_objects', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_objects', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_objects', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_object_components', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_conflict_snapshots', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_conflict_snapshots', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_objects', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('dav_conflict_snapshots', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('task_lists', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('task_lists', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tasks', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tasks', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_objects', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tasks', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_object_components', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('tasks', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('pending_ops', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('pending_ops', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_objects', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('pending_ops', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('pending_ops', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_conflict_snapshots', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('pending_ops', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('sync_runs', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_sources', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_sources', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_events', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'calendar_sources', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_events', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_events', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_objects', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_events', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_object_components', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('calendar_events', kind: UpdateKind.update)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'calendar_events', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('calendar_event_attendees', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'calendar_events', + limitUpdateKind: UpdateKind.delete, + ), + result: [ + TableUpdate('calendar_event_reminders', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('sync_cursors', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'calendar_sources', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('sync_cursors', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'dav_collections', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('sync_cursors', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('schedule_item_overrides', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'accounts', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('notification_schedule', kind: UpdateKind.delete)], + ), + ]); +} + +typedef $$AccountsTableCreateCompanionBuilder = + AccountsCompanion Function({ + required String id, + required String provider, + required String authority, + required String providerAccountId, + required String credentialKind, + Value providerProfileVersion, + Value displayName, + Value email, + Value tenantId, + Value accountAvatarUrl, + Value providerMetadataJson, + Value authState, + Value calendarsEnabled, + Value tasksEnabled, + Value grantedScopes, + required String createdAtUtc, + required String updatedAtUtc, + Value lastSuccessfulSyncAtUtc, + Value lastFullSyncAtUtc, + Value rowid, + }); +typedef $$AccountsTableUpdateCompanionBuilder = + AccountsCompanion Function({ + Value id, + Value provider, + Value authority, + Value providerAccountId, + Value credentialKind, + Value providerProfileVersion, + Value displayName, + Value email, + Value tenantId, + Value accountAvatarUrl, + Value providerMetadataJson, + Value authState, + Value calendarsEnabled, + Value tasksEnabled, + Value grantedScopes, + Value createdAtUtc, + Value updatedAtUtc, + Value lastSuccessfulSyncAtUtc, + Value lastFullSyncAtUtc, + Value rowid, + }); + +final class $$AccountsTableReferences + extends BaseReferences<_$AppDatabase, $AccountsTable, Account> { + $$AccountsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$DavAccountServicesTable, List> + _davAccountServicesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.davAccountServices, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.davAccountServices.accountId, + ), + ); + + $$DavAccountServicesTableProcessedTableManager get davAccountServicesRefs { + final manager = $$DavAccountServicesTableTableManager( + $_db, + $_db.davAccountServices, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _davAccountServicesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$DavCollectionsTable, List> + _davCollectionsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.davCollections, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.davCollections.accountId, + ), + ); + + $$DavCollectionsTableProcessedTableManager get davCollectionsRefs { + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_davCollectionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$DavObjectsTable, List> + _davObjectsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.davObjects, + aliasName: $_aliasNameGenerator(db.accounts.id, db.davObjects.accountId), + ); + + $$DavObjectsTableProcessedTableManager get davObjectsRefs { + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_davObjectsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $DavConflictSnapshotsTable, + List + > + _davConflictSnapshotsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.davConflictSnapshots, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.davConflictSnapshots.accountId, + ), + ); + + $$DavConflictSnapshotsTableProcessedTableManager + get davConflictSnapshotsRefs { + final manager = $$DavConflictSnapshotsTableTableManager( + $_db, + $_db.davConflictSnapshots, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _davConflictSnapshotsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TaskListsTable, List> + _taskListsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.taskLists, + aliasName: $_aliasNameGenerator(db.accounts.id, db.taskLists.accountId), + ); + + $$TaskListsTableProcessedTableManager get taskListsRefs { + final manager = $$TaskListsTableTableManager( + $_db, + $_db.taskLists, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_taskListsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TasksTable, List> _tasksRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.tasks, + aliasName: $_aliasNameGenerator(db.accounts.id, db.tasks.accountId), + ); + + $$TasksTableProcessedTableManager get tasksRefs { + final manager = $$TasksTableTableManager( + $_db, + $_db.tasks, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_tasksRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PendingOpsTable, List> + _pendingOpsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.pendingOps, + aliasName: $_aliasNameGenerator(db.accounts.id, db.pendingOps.accountId), + ); + + $$PendingOpsTableProcessedTableManager get pendingOpsRefs { + final manager = $$PendingOpsTableTableManager( + $_db, + $_db.pendingOps, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_pendingOpsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$SyncRunsTable, List> _syncRunsRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.syncRuns, + aliasName: $_aliasNameGenerator(db.accounts.id, db.syncRuns.accountId), + ); + + $$SyncRunsTableProcessedTableManager get syncRunsRefs { + final manager = $$SyncRunsTableTableManager( + $_db, + $_db.syncRuns, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_syncRunsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarSourcesTable, List> + _calendarSourcesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarSources, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.calendarSources.accountId, + ), + ); + + $$CalendarSourcesTableProcessedTableManager get calendarSourcesRefs { + final manager = $$CalendarSourcesTableTableManager( + $_db, + $_db.calendarSources, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _calendarSourcesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarEventsTable, List> + _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarEvents, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.calendarEvents.accountId, + ), + ); + + $$CalendarEventsTableProcessedTableManager get calendarEventsRefs { + final manager = $$CalendarEventsTableTableManager( + $_db, + $_db.calendarEvents, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_calendarEventsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$SyncCursorsTable, List> + _syncCursorsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.syncCursors, + aliasName: $_aliasNameGenerator(db.accounts.id, db.syncCursors.accountId), + ); + + $$SyncCursorsTableProcessedTableManager get syncCursorsRefs { + final manager = $$SyncCursorsTableTableManager( + $_db, + $_db.syncCursors, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_syncCursorsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $ScheduleItemOverridesTable, + List + > + _scheduleItemOverridesRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.scheduleItemOverrides, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.scheduleItemOverrides.accountId, + ), + ); + + $$ScheduleItemOverridesTableProcessedTableManager + get scheduleItemOverridesRefs { + final manager = $$ScheduleItemOverridesTableTableManager( + $_db, + $_db.scheduleItemOverrides, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _scheduleItemOverridesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $NotificationScheduleTable, + List + > + _notificationScheduleRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.notificationSchedule, + aliasName: $_aliasNameGenerator( + db.accounts.id, + db.notificationSchedule.accountId, + ), + ); + + $$NotificationScheduleTableProcessedTableManager + get notificationScheduleRefs { + final manager = $$NotificationScheduleTableTableManager( + $_db, + $_db.notificationSchedule, + ).filter((f) => f.accountId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _notificationScheduleRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$AccountsTableFilterComposer + extends Composer<_$AppDatabase, $AccountsTable> { + $$AccountsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get provider => $composableBuilder( + column: $table.provider, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get authority => $composableBuilder( + column: $table.authority, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerAccountId => $composableBuilder( + column: $table.providerAccountId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get credentialKind => $composableBuilder( + column: $table.credentialKind, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tenantId => $composableBuilder( + column: $table.tenantId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get accountAvatarUrl => $composableBuilder( + column: $table.accountAvatarUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerMetadataJson => $composableBuilder( + column: $table.providerMetadataJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get authState => $composableBuilder( + column: $table.authState, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get calendarsEnabled => $composableBuilder( + column: $table.calendarsEnabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tasksEnabled => $composableBuilder( + column: $table.tasksEnabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get grantedScopes => $composableBuilder( + column: $table.grantedScopes, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastSuccessfulSyncAtUtc => $composableBuilder( + column: $table.lastSuccessfulSyncAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastFullSyncAtUtc => $composableBuilder( + column: $table.lastFullSyncAtUtc, + builder: (column) => ColumnFilters(column), + ); + + Expression davAccountServicesRefs( + Expression Function($$DavAccountServicesTableFilterComposer f) f, + ) { + final $$DavAccountServicesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davAccountServices, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavAccountServicesTableFilterComposer( + $db: $db, + $table: $db.davAccountServices, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davCollectionsRefs( + Expression Function($$DavCollectionsTableFilterComposer f) f, + ) { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davObjectsRefs( + Expression Function($$DavObjectsTableFilterComposer f) f, + ) { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableFilterComposer f) f, + ) { + final $$DavConflictSnapshotsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableFilterComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression taskListsRefs( + Expression Function($$TaskListsTableFilterComposer f) f, + ) { + final $$TaskListsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.taskLists, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TaskListsTableFilterComposer( + $db: $db, + $table: $db.taskLists, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableFilterComposer f) f, + ) { + final $$TasksTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableFilterComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableFilterComposer f) f, + ) { + final $$PendingOpsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableFilterComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncRunsRefs( + Expression Function($$SyncRunsTableFilterComposer f) f, + ) { + final $$SyncRunsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncRuns, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncRunsTableFilterComposer( + $db: $db, + $table: $db.syncRuns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarSourcesRefs( + Expression Function($$CalendarSourcesTableFilterComposer f) f, + ) { + final $$CalendarSourcesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarSources, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarSourcesTableFilterComposer( + $db: $db, + $table: $db.calendarSources, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableFilterComposer f) f, + ) { + final $$CalendarEventsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableFilterComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableFilterComposer f) f, + ) { + final $$SyncCursorsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncCursorsTableFilterComposer( + $db: $db, + $table: $db.syncCursors, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression scheduleItemOverridesRefs( + Expression Function($$ScheduleItemOverridesTableFilterComposer f) f, + ) { + final $$ScheduleItemOverridesTableFilterComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.scheduleItemOverrides, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ScheduleItemOverridesTableFilterComposer( + $db: $db, + $table: $db.scheduleItemOverrides, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression notificationScheduleRefs( + Expression Function($$NotificationScheduleTableFilterComposer f) f, + ) { + final $$NotificationScheduleTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.notificationSchedule, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$NotificationScheduleTableFilterComposer( + $db: $db, + $table: $db.notificationSchedule, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$AccountsTableOrderingComposer + extends Composer<_$AppDatabase, $AccountsTable> { + $$AccountsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get provider => $composableBuilder( + column: $table.provider, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get authority => $composableBuilder( + column: $table.authority, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerAccountId => $composableBuilder( + column: $table.providerAccountId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get credentialKind => $composableBuilder( + column: $table.credentialKind, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get email => $composableBuilder( + column: $table.email, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tenantId => $composableBuilder( + column: $table.tenantId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get accountAvatarUrl => $composableBuilder( + column: $table.accountAvatarUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerMetadataJson => $composableBuilder( + column: $table.providerMetadataJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get authState => $composableBuilder( + column: $table.authState, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get calendarsEnabled => $composableBuilder( + column: $table.calendarsEnabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tasksEnabled => $composableBuilder( + column: $table.tasksEnabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get grantedScopes => $composableBuilder( + column: $table.grantedScopes, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastSuccessfulSyncAtUtc => $composableBuilder( + column: $table.lastSuccessfulSyncAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastFullSyncAtUtc => $composableBuilder( + column: $table.lastFullSyncAtUtc, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$AccountsTableAnnotationComposer + extends Composer<_$AppDatabase, $AccountsTable> { + $$AccountsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get provider => + $composableBuilder(column: $table.provider, builder: (column) => column); + + GeneratedColumn get authority => + $composableBuilder(column: $table.authority, builder: (column) => column); + + GeneratedColumn get providerAccountId => $composableBuilder( + column: $table.providerAccountId, + builder: (column) => column, + ); + + GeneratedColumn get credentialKind => $composableBuilder( + column: $table.credentialKind, + builder: (column) => column, + ); + + GeneratedColumn get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => column, + ); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); + + GeneratedColumn get email => + $composableBuilder(column: $table.email, builder: (column) => column); + + GeneratedColumn get tenantId => + $composableBuilder(column: $table.tenantId, builder: (column) => column); + + GeneratedColumn get accountAvatarUrl => $composableBuilder( + column: $table.accountAvatarUrl, + builder: (column) => column, + ); + + GeneratedColumn get providerMetadataJson => $composableBuilder( + column: $table.providerMetadataJson, + builder: (column) => column, + ); + + GeneratedColumn get authState => + $composableBuilder(column: $table.authState, builder: (column) => column); + + GeneratedColumn get calendarsEnabled => $composableBuilder( + column: $table.calendarsEnabled, + builder: (column) => column, + ); + + GeneratedColumn get tasksEnabled => $composableBuilder( + column: $table.tasksEnabled, + builder: (column) => column, + ); + + GeneratedColumn get grantedScopes => $composableBuilder( + column: $table.grantedScopes, + builder: (column) => column, + ); + + GeneratedColumn get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastSuccessfulSyncAtUtc => $composableBuilder( + column: $table.lastSuccessfulSyncAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastFullSyncAtUtc => $composableBuilder( + column: $table.lastFullSyncAtUtc, + builder: (column) => column, + ); + + Expression davAccountServicesRefs( + Expression Function($$DavAccountServicesTableAnnotationComposer a) f, + ) { + final $$DavAccountServicesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davAccountServices, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavAccountServicesTableAnnotationComposer( + $db: $db, + $table: $db.davAccountServices, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davCollectionsRefs( + Expression Function($$DavCollectionsTableAnnotationComposer a) f, + ) { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davObjectsRefs( + Expression Function($$DavObjectsTableAnnotationComposer a) f, + ) { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableAnnotationComposer a) f, + ) { + final $$DavConflictSnapshotsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableAnnotationComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression taskListsRefs( + Expression Function($$TaskListsTableAnnotationComposer a) f, + ) { + final $$TaskListsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.taskLists, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TaskListsTableAnnotationComposer( + $db: $db, + $table: $db.taskLists, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableAnnotationComposer a) f, + ) { + final $$TasksTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableAnnotationComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableAnnotationComposer a) f, + ) { + final $$PendingOpsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableAnnotationComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncRunsRefs( + Expression Function($$SyncRunsTableAnnotationComposer a) f, + ) { + final $$SyncRunsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncRuns, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncRunsTableAnnotationComposer( + $db: $db, + $table: $db.syncRuns, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarSourcesRefs( + Expression Function($$CalendarSourcesTableAnnotationComposer a) f, + ) { + final $$CalendarSourcesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarSources, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarSourcesTableAnnotationComposer( + $db: $db, + $table: $db.calendarSources, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableAnnotationComposer a) f, + ) { + final $$CalendarEventsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableAnnotationComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableAnnotationComposer a) f, + ) { + final $$SyncCursorsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncCursorsTableAnnotationComposer( + $db: $db, + $table: $db.syncCursors, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression scheduleItemOverridesRefs( + Expression Function($$ScheduleItemOverridesTableAnnotationComposer a) f, + ) { + final $$ScheduleItemOverridesTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.scheduleItemOverrides, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ScheduleItemOverridesTableAnnotationComposer( + $db: $db, + $table: $db.scheduleItemOverrides, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression notificationScheduleRefs( + Expression Function($$NotificationScheduleTableAnnotationComposer a) f, + ) { + final $$NotificationScheduleTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.notificationSchedule, + getReferencedColumn: (t) => t.accountId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$NotificationScheduleTableAnnotationComposer( + $db: $db, + $table: $db.notificationSchedule, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$AccountsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $AccountsTable, + Account, + $$AccountsTableFilterComposer, + $$AccountsTableOrderingComposer, + $$AccountsTableAnnotationComposer, + $$AccountsTableCreateCompanionBuilder, + $$AccountsTableUpdateCompanionBuilder, + (Account, $$AccountsTableReferences), + Account, + PrefetchHooks Function({ + bool davAccountServicesRefs, + bool davCollectionsRefs, + bool davObjectsRefs, + bool davConflictSnapshotsRefs, + bool taskListsRefs, + bool tasksRefs, + bool pendingOpsRefs, + bool syncRunsRefs, + bool calendarSourcesRefs, + bool calendarEventsRefs, + bool syncCursorsRefs, + bool scheduleItemOverridesRefs, + bool notificationScheduleRefs, + }) + > { + $$AccountsTableTableManager(_$AppDatabase db, $AccountsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$AccountsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$AccountsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$AccountsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value provider = const Value.absent(), + Value authority = const Value.absent(), + Value providerAccountId = const Value.absent(), + Value credentialKind = const Value.absent(), + Value providerProfileVersion = const Value.absent(), + Value displayName = const Value.absent(), + Value email = const Value.absent(), + Value tenantId = const Value.absent(), + Value accountAvatarUrl = const Value.absent(), + Value providerMetadataJson = const Value.absent(), + Value authState = const Value.absent(), + Value calendarsEnabled = const Value.absent(), + Value tasksEnabled = const Value.absent(), + Value grantedScopes = const Value.absent(), + Value createdAtUtc = const Value.absent(), + Value updatedAtUtc = const Value.absent(), + Value lastSuccessfulSyncAtUtc = const Value.absent(), + Value lastFullSyncAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => AccountsCompanion( + id: id, + provider: provider, + authority: authority, + providerAccountId: providerAccountId, + credentialKind: credentialKind, + providerProfileVersion: providerProfileVersion, + displayName: displayName, + email: email, + tenantId: tenantId, + accountAvatarUrl: accountAvatarUrl, + providerMetadataJson: providerMetadataJson, + authState: authState, + calendarsEnabled: calendarsEnabled, + tasksEnabled: tasksEnabled, + grantedScopes: grantedScopes, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + lastSuccessfulSyncAtUtc: lastSuccessfulSyncAtUtc, + lastFullSyncAtUtc: lastFullSyncAtUtc, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String provider, + required String authority, + required String providerAccountId, + required String credentialKind, + Value providerProfileVersion = const Value.absent(), + Value displayName = const Value.absent(), + Value email = const Value.absent(), + Value tenantId = const Value.absent(), + Value accountAvatarUrl = const Value.absent(), + Value providerMetadataJson = const Value.absent(), + Value authState = const Value.absent(), + Value calendarsEnabled = const Value.absent(), + Value tasksEnabled = const Value.absent(), + Value grantedScopes = const Value.absent(), + required String createdAtUtc, + required String updatedAtUtc, + Value lastSuccessfulSyncAtUtc = const Value.absent(), + Value lastFullSyncAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => AccountsCompanion.insert( + id: id, + provider: provider, + authority: authority, + providerAccountId: providerAccountId, + credentialKind: credentialKind, + providerProfileVersion: providerProfileVersion, + displayName: displayName, + email: email, + tenantId: tenantId, + accountAvatarUrl: accountAvatarUrl, + providerMetadataJson: providerMetadataJson, + authState: authState, + calendarsEnabled: calendarsEnabled, + tasksEnabled: tasksEnabled, + grantedScopes: grantedScopes, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + lastSuccessfulSyncAtUtc: lastSuccessfulSyncAtUtc, + lastFullSyncAtUtc: lastFullSyncAtUtc, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$AccountsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + davAccountServicesRefs = false, + davCollectionsRefs = false, + davObjectsRefs = false, + davConflictSnapshotsRefs = false, + taskListsRefs = false, + tasksRefs = false, + pendingOpsRefs = false, + syncRunsRefs = false, + calendarSourcesRefs = false, + calendarEventsRefs = false, + syncCursorsRefs = false, + scheduleItemOverridesRefs = false, + notificationScheduleRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (davAccountServicesRefs) db.davAccountServices, + if (davCollectionsRefs) db.davCollections, + if (davObjectsRefs) db.davObjects, + if (davConflictSnapshotsRefs) db.davConflictSnapshots, + if (taskListsRefs) db.taskLists, + if (tasksRefs) db.tasks, + if (pendingOpsRefs) db.pendingOps, + if (syncRunsRefs) db.syncRuns, + if (calendarSourcesRefs) db.calendarSources, + if (calendarEventsRefs) db.calendarEvents, + if (syncCursorsRefs) db.syncCursors, + if (scheduleItemOverridesRefs) db.scheduleItemOverrides, + if (notificationScheduleRefs) db.notificationSchedule, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (davAccountServicesRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + DavAccountService + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._davAccountServicesRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).davAccountServicesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (davCollectionsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + DavCollection + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._davCollectionsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).davCollectionsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (davObjectsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + DavObject + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._davObjectsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).davObjectsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (davConflictSnapshotsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + DavConflictSnapshot + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._davConflictSnapshotsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).davConflictSnapshotsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (taskListsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + TaskList + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._taskListsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).taskListsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (tasksRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + Task + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._tasksRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).tasksRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (pendingOpsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + PendingOp + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._pendingOpsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).pendingOpsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (syncRunsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + SyncRun + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._syncRunsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).syncRunsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (calendarSourcesRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + CalendarSource + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._calendarSourcesRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).calendarSourcesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (calendarEventsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + CalendarEvent + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._calendarEventsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).calendarEventsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (syncCursorsRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + SyncCursor + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._syncCursorsRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).syncCursorsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (scheduleItemOverridesRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + ScheduleItemOverride + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._scheduleItemOverridesRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).scheduleItemOverridesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + if (notificationScheduleRefs) + await $_getPrefetchedData< + Account, + $AccountsTable, + NotificationScheduleData + >( + currentTable: table, + referencedTable: $$AccountsTableReferences + ._notificationScheduleRefsTable(db), + managerFromTypedResult: (p0) => + $$AccountsTableReferences( + db, + table, + p0, + ).notificationScheduleRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.accountId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$AccountsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $AccountsTable, + Account, + $$AccountsTableFilterComposer, + $$AccountsTableOrderingComposer, + $$AccountsTableAnnotationComposer, + $$AccountsTableCreateCompanionBuilder, + $$AccountsTableUpdateCompanionBuilder, + (Account, $$AccountsTableReferences), + Account, + PrefetchHooks Function({ + bool davAccountServicesRefs, + bool davCollectionsRefs, + bool davObjectsRefs, + bool davConflictSnapshotsRefs, + bool taskListsRefs, + bool tasksRefs, + bool pendingOpsRefs, + bool syncRunsRefs, + bool calendarSourcesRefs, + bool calendarEventsRefs, + bool syncCursorsRefs, + bool scheduleItemOverridesRefs, + bool notificationScheduleRefs, + }) + >; +typedef $$DavAccountServicesTableCreateCompanionBuilder = + DavAccountServicesCompanion Function({ + required String accountId, + required String canonicalServiceUri, + required String canonicalOrigin, + Value principalHref, + Value calendarHomeHref, + Value calendarUserAddressesJson, + Value scheduleInboxHref, + Value scheduleOutboxHref, + Value capabilitiesJson, + Value capabilitiesSchemaVersion, + Value providerProfileVersion, + required String discoveredAtUtc, + Value lastValidatedAtUtc, + Value lastDiscoveryErrorCode, + Value rowid, + }); +typedef $$DavAccountServicesTableUpdateCompanionBuilder = + DavAccountServicesCompanion Function({ + Value accountId, + Value canonicalServiceUri, + Value canonicalOrigin, + Value principalHref, + Value calendarHomeHref, + Value calendarUserAddressesJson, + Value scheduleInboxHref, + Value scheduleOutboxHref, + Value capabilitiesJson, + Value capabilitiesSchemaVersion, + Value providerProfileVersion, + Value discoveredAtUtc, + Value lastValidatedAtUtc, + Value lastDiscoveryErrorCode, + Value rowid, + }); + +final class $$DavAccountServicesTableReferences + extends + BaseReferences< + _$AppDatabase, + $DavAccountServicesTable, + DavAccountService + > { + $$DavAccountServicesTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $AccountsTable _accountIdTable(_$AppDatabase db) => + db.accounts.createAlias( + $_aliasNameGenerator(db.davAccountServices.accountId, db.accounts.id), + ); + + $$AccountsTableProcessedTableManager get accountId { + final $_column = $_itemColumn('account_id')!; + + final manager = $$AccountsTableTableManager( + $_db, + $_db.accounts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$DavAccountServicesTableFilterComposer + extends Composer<_$AppDatabase, $DavAccountServicesTable> { + $$DavAccountServicesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get canonicalServiceUri => $composableBuilder( + column: $table.canonicalServiceUri, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get canonicalOrigin => $composableBuilder( + column: $table.canonicalOrigin, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get principalHref => $composableBuilder( + column: $table.principalHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get calendarHomeHref => $composableBuilder( + column: $table.calendarHomeHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get calendarUserAddressesJson => $composableBuilder( + column: $table.calendarUserAddressesJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get scheduleInboxHref => $composableBuilder( + column: $table.scheduleInboxHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get scheduleOutboxHref => $composableBuilder( + column: $table.scheduleOutboxHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get capabilitiesJson => $composableBuilder( + column: $table.capabilitiesJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get capabilitiesSchemaVersion => $composableBuilder( + column: $table.capabilitiesSchemaVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get discoveredAtUtc => $composableBuilder( + column: $table.discoveredAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastValidatedAtUtc => $composableBuilder( + column: $table.lastValidatedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastDiscoveryErrorCode => $composableBuilder( + column: $table.lastDiscoveryErrorCode, + builder: (column) => ColumnFilters(column), + ); + + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavAccountServicesTableOrderingComposer + extends Composer<_$AppDatabase, $DavAccountServicesTable> { + $$DavAccountServicesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get canonicalServiceUri => $composableBuilder( + column: $table.canonicalServiceUri, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get canonicalOrigin => $composableBuilder( + column: $table.canonicalOrigin, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get principalHref => $composableBuilder( + column: $table.principalHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get calendarHomeHref => $composableBuilder( + column: $table.calendarHomeHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get calendarUserAddressesJson => $composableBuilder( + column: $table.calendarUserAddressesJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get scheduleInboxHref => $composableBuilder( + column: $table.scheduleInboxHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get scheduleOutboxHref => $composableBuilder( + column: $table.scheduleOutboxHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get capabilitiesJson => $composableBuilder( + column: $table.capabilitiesJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get capabilitiesSchemaVersion => $composableBuilder( + column: $table.capabilitiesSchemaVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get discoveredAtUtc => $composableBuilder( + column: $table.discoveredAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastValidatedAtUtc => $composableBuilder( + column: $table.lastValidatedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastDiscoveryErrorCode => $composableBuilder( + column: $table.lastDiscoveryErrorCode, + builder: (column) => ColumnOrderings(column), + ); + + $$AccountsTableOrderingComposer get accountId { + final $$AccountsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableOrderingComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavAccountServicesTableAnnotationComposer + extends Composer<_$AppDatabase, $DavAccountServicesTable> { + $$DavAccountServicesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get canonicalServiceUri => $composableBuilder( + column: $table.canonicalServiceUri, + builder: (column) => column, + ); + + GeneratedColumn get canonicalOrigin => $composableBuilder( + column: $table.canonicalOrigin, + builder: (column) => column, + ); + + GeneratedColumn get principalHref => $composableBuilder( + column: $table.principalHref, + builder: (column) => column, + ); + + GeneratedColumn get calendarHomeHref => $composableBuilder( + column: $table.calendarHomeHref, + builder: (column) => column, + ); + + GeneratedColumn get calendarUserAddressesJson => $composableBuilder( + column: $table.calendarUserAddressesJson, + builder: (column) => column, + ); + + GeneratedColumn get scheduleInboxHref => $composableBuilder( + column: $table.scheduleInboxHref, + builder: (column) => column, + ); + + GeneratedColumn get scheduleOutboxHref => $composableBuilder( + column: $table.scheduleOutboxHref, + builder: (column) => column, + ); + + GeneratedColumn get capabilitiesJson => $composableBuilder( + column: $table.capabilitiesJson, + builder: (column) => column, + ); + + GeneratedColumn get capabilitiesSchemaVersion => $composableBuilder( + column: $table.capabilitiesSchemaVersion, + builder: (column) => column, + ); + + GeneratedColumn get providerProfileVersion => $composableBuilder( + column: $table.providerProfileVersion, + builder: (column) => column, + ); + + GeneratedColumn get discoveredAtUtc => $composableBuilder( + column: $table.discoveredAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastValidatedAtUtc => $composableBuilder( + column: $table.lastValidatedAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastDiscoveryErrorCode => $composableBuilder( + column: $table.lastDiscoveryErrorCode, + builder: (column) => column, + ); + + $$AccountsTableAnnotationComposer get accountId { + final $$AccountsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableAnnotationComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavAccountServicesTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DavAccountServicesTable, + DavAccountService, + $$DavAccountServicesTableFilterComposer, + $$DavAccountServicesTableOrderingComposer, + $$DavAccountServicesTableAnnotationComposer, + $$DavAccountServicesTableCreateCompanionBuilder, + $$DavAccountServicesTableUpdateCompanionBuilder, + (DavAccountService, $$DavAccountServicesTableReferences), + DavAccountService, + PrefetchHooks Function({bool accountId}) + > { + $$DavAccountServicesTableTableManager( + _$AppDatabase db, + $DavAccountServicesTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DavAccountServicesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DavAccountServicesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DavAccountServicesTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value accountId = const Value.absent(), + Value canonicalServiceUri = const Value.absent(), + Value canonicalOrigin = const Value.absent(), + Value principalHref = const Value.absent(), + Value calendarHomeHref = const Value.absent(), + Value calendarUserAddressesJson = const Value.absent(), + Value scheduleInboxHref = const Value.absent(), + Value scheduleOutboxHref = const Value.absent(), + Value capabilitiesJson = const Value.absent(), + Value capabilitiesSchemaVersion = const Value.absent(), + Value providerProfileVersion = const Value.absent(), + Value discoveredAtUtc = const Value.absent(), + Value lastValidatedAtUtc = const Value.absent(), + Value lastDiscoveryErrorCode = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavAccountServicesCompanion( + accountId: accountId, + canonicalServiceUri: canonicalServiceUri, + canonicalOrigin: canonicalOrigin, + principalHref: principalHref, + calendarHomeHref: calendarHomeHref, + calendarUserAddressesJson: calendarUserAddressesJson, + scheduleInboxHref: scheduleInboxHref, + scheduleOutboxHref: scheduleOutboxHref, + capabilitiesJson: capabilitiesJson, + capabilitiesSchemaVersion: capabilitiesSchemaVersion, + providerProfileVersion: providerProfileVersion, + discoveredAtUtc: discoveredAtUtc, + lastValidatedAtUtc: lastValidatedAtUtc, + lastDiscoveryErrorCode: lastDiscoveryErrorCode, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String accountId, + required String canonicalServiceUri, + required String canonicalOrigin, + Value principalHref = const Value.absent(), + Value calendarHomeHref = const Value.absent(), + Value calendarUserAddressesJson = const Value.absent(), + Value scheduleInboxHref = const Value.absent(), + Value scheduleOutboxHref = const Value.absent(), + Value capabilitiesJson = const Value.absent(), + Value capabilitiesSchemaVersion = const Value.absent(), + Value providerProfileVersion = const Value.absent(), + required String discoveredAtUtc, + Value lastValidatedAtUtc = const Value.absent(), + Value lastDiscoveryErrorCode = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavAccountServicesCompanion.insert( + accountId: accountId, + canonicalServiceUri: canonicalServiceUri, + canonicalOrigin: canonicalOrigin, + principalHref: principalHref, + calendarHomeHref: calendarHomeHref, + calendarUserAddressesJson: calendarUserAddressesJson, + scheduleInboxHref: scheduleInboxHref, + scheduleOutboxHref: scheduleOutboxHref, + capabilitiesJson: capabilitiesJson, + capabilitiesSchemaVersion: capabilitiesSchemaVersion, + providerProfileVersion: providerProfileVersion, + discoveredAtUtc: discoveredAtUtc, + lastValidatedAtUtc: lastValidatedAtUtc, + lastDiscoveryErrorCode: lastDiscoveryErrorCode, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$DavAccountServicesTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({accountId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: + $$DavAccountServicesTableReferences + ._accountIdTable(db), + referencedColumn: + $$DavAccountServicesTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$DavAccountServicesTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DavAccountServicesTable, + DavAccountService, + $$DavAccountServicesTableFilterComposer, + $$DavAccountServicesTableOrderingComposer, + $$DavAccountServicesTableAnnotationComposer, + $$DavAccountServicesTableCreateCompanionBuilder, + $$DavAccountServicesTableUpdateCompanionBuilder, + (DavAccountService, $$DavAccountServicesTableReferences), + DavAccountService, + PrefetchHooks Function({bool accountId}) + >; +typedef $$DavCollectionsTableCreateCompanionBuilder = + DavCollectionsCompanion Function({ + required String id, + required String accountId, + required String hrefKey, + required String requestUri, + required String displayName, + Value description, + Value resourceTypesJson, + Value supportedComponentMask, + Value supportedCalendarDataJson, + Value supportedReportsJson, + Value currentUserPrivilegesJson, + Value ownerHref, + Value safeDisplayMetadataJson, + Value color, + Value sortOrder, + Value calendarTimeZone, + Value calendarTimeZoneId, + Value scheduleTransparency, + Value maximumResourceSize, + Value maximumInstances, + Value syncToken, + Value ctag, + Value readOnly, + Value eventProjectionEnabled, + Value taskProjectionEnabled, + Value eventsSelected, + Value tasksSelected, + Value serverMissing, + Value deleted, + Value lastInventoryAtUtc, + Value lastSyncAtUtc, + Value parserVersion, + Value projectionVersion, + required String createdAtUtc, + required String updatedAtUtc, + Value rowid, + }); +typedef $$DavCollectionsTableUpdateCompanionBuilder = + DavCollectionsCompanion Function({ + Value id, + Value accountId, + Value hrefKey, + Value requestUri, + Value displayName, + Value description, + Value resourceTypesJson, + Value supportedComponentMask, + Value supportedCalendarDataJson, + Value supportedReportsJson, + Value currentUserPrivilegesJson, + Value ownerHref, + Value safeDisplayMetadataJson, + Value color, + Value sortOrder, + Value calendarTimeZone, + Value calendarTimeZoneId, + Value scheduleTransparency, + Value maximumResourceSize, + Value maximumInstances, + Value syncToken, + Value ctag, + Value readOnly, + Value eventProjectionEnabled, + Value taskProjectionEnabled, + Value eventsSelected, + Value tasksSelected, + Value serverMissing, + Value deleted, + Value lastInventoryAtUtc, + Value lastSyncAtUtc, + Value parserVersion, + Value projectionVersion, + Value createdAtUtc, + Value updatedAtUtc, + Value rowid, + }); + +final class $$DavCollectionsTableReferences + extends BaseReferences<_$AppDatabase, $DavCollectionsTable, DavCollection> { + $$DavCollectionsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $AccountsTable _accountIdTable(_$AppDatabase db) => + db.accounts.createAlias( + $_aliasNameGenerator(db.davCollections.accountId, db.accounts.id), + ); + + $$AccountsTableProcessedTableManager get accountId { + final $_column = $_itemColumn('account_id')!; + + final manager = $$AccountsTableTableManager( + $_db, + $_db.accounts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$DavObjectsTable, List> + _davObjectsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.davObjects, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.davObjects.collectionId, + ), + ); + + $$DavObjectsTableProcessedTableManager get davObjectsRefs { + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.collectionId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_davObjectsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $DavConflictSnapshotsTable, + List + > + _davConflictSnapshotsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.davConflictSnapshots, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.davConflictSnapshots.davCollectionId, + ), + ); + + $$DavConflictSnapshotsTableProcessedTableManager + get davConflictSnapshotsRefs { + final manager = + $$DavConflictSnapshotsTableTableManager( + $_db, + $_db.davConflictSnapshots, + ).filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _davConflictSnapshotsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TaskListsTable, List> + _taskListsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.taskLists, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.taskLists.davCollectionId, + ), + ); + + $$TaskListsTableProcessedTableManager get taskListsRefs { + final manager = $$TaskListsTableTableManager($_db, $_db.taskLists).filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull(_taskListsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TasksTable, List> _tasksRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.tasks, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.tasks.davCollectionId, + ), + ); + + $$TasksTableProcessedTableManager get tasksRefs { + final manager = $$TasksTableTableManager($_db, $_db.tasks).filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull(_tasksRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarSourcesTable, List> + _calendarSourcesRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarSources, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.calendarSources.davCollectionId, + ), + ); + + $$CalendarSourcesTableProcessedTableManager get calendarSourcesRefs { + final manager = + $$CalendarSourcesTableTableManager($_db, $_db.calendarSources).filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull( + _calendarSourcesRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarEventsTable, List> + _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarEvents, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.calendarEvents.davCollectionId, + ), + ); + + $$CalendarEventsTableProcessedTableManager get calendarEventsRefs { + final manager = $$CalendarEventsTableTableManager($_db, $_db.calendarEvents) + .filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull(_calendarEventsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$SyncCursorsTable, List> + _syncCursorsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.syncCursors, + aliasName: $_aliasNameGenerator( + db.davCollections.id, + db.syncCursors.davCollectionId, + ), + ); + + $$SyncCursorsTableProcessedTableManager get syncCursorsRefs { + final manager = $$SyncCursorsTableTableManager($_db, $_db.syncCursors) + .filter( + (f) => f.davCollectionId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull(_syncCursorsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$DavCollectionsTableFilterComposer + extends Composer<_$AppDatabase, $DavCollectionsTable> { + $$DavCollectionsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get hrefKey => $composableBuilder( + column: $table.hrefKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resourceTypesJson => $composableBuilder( + column: $table.resourceTypesJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supportedComponentMask => $composableBuilder( + column: $table.supportedComponentMask, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supportedCalendarDataJson => $composableBuilder( + column: $table.supportedCalendarDataJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get supportedReportsJson => $composableBuilder( + column: $table.supportedReportsJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get currentUserPrivilegesJson => $composableBuilder( + column: $table.currentUserPrivilegesJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get ownerHref => $composableBuilder( + column: $table.ownerHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get safeDisplayMetadataJson => $composableBuilder( + column: $table.safeDisplayMetadataJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get color => $composableBuilder( + column: $table.color, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get calendarTimeZone => $composableBuilder( + column: $table.calendarTimeZone, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get calendarTimeZoneId => $composableBuilder( + column: $table.calendarTimeZoneId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get scheduleTransparency => $composableBuilder( + column: $table.scheduleTransparency, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maximumResourceSize => $composableBuilder( + column: $table.maximumResourceSize, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get maximumInstances => $composableBuilder( + column: $table.maximumInstances, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncToken => $composableBuilder( + column: $table.syncToken, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get ctag => $composableBuilder( + column: $table.ctag, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get readOnly => $composableBuilder( + column: $table.readOnly, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get eventProjectionEnabled => $composableBuilder( + column: $table.eventProjectionEnabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskProjectionEnabled => $composableBuilder( + column: $table.taskProjectionEnabled, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get eventsSelected => $composableBuilder( + column: $table.eventsSelected, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get tasksSelected => $composableBuilder( + column: $table.tasksSelected, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverMissing => $composableBuilder( + column: $table.serverMissing, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deleted => $composableBuilder( + column: $table.deleted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastInventoryAtUtc => $composableBuilder( + column: $table.lastInventoryAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastSyncAtUtc => $composableBuilder( + column: $table.lastSyncAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression davObjectsRefs( + Expression Function($$DavObjectsTableFilterComposer f) f, + ) { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.collectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableFilterComposer f) f, + ) { + final $$DavConflictSnapshotsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableFilterComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression taskListsRefs( + Expression Function($$TaskListsTableFilterComposer f) f, + ) { + final $$TaskListsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.taskLists, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TaskListsTableFilterComposer( + $db: $db, + $table: $db.taskLists, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableFilterComposer f) f, + ) { + final $$TasksTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableFilterComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarSourcesRefs( + Expression Function($$CalendarSourcesTableFilterComposer f) f, + ) { + final $$CalendarSourcesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarSources, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarSourcesTableFilterComposer( + $db: $db, + $table: $db.calendarSources, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableFilterComposer f) f, + ) { + final $$CalendarEventsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableFilterComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableFilterComposer f) f, + ) { + final $$SyncCursorsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncCursorsTableFilterComposer( + $db: $db, + $table: $db.syncCursors, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavCollectionsTableOrderingComposer + extends Composer<_$AppDatabase, $DavCollectionsTable> { + $$DavCollectionsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get hrefKey => $composableBuilder( + column: $table.hrefKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resourceTypesJson => $composableBuilder( + column: $table.resourceTypesJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supportedComponentMask => $composableBuilder( + column: $table.supportedComponentMask, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supportedCalendarDataJson => $composableBuilder( + column: $table.supportedCalendarDataJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get supportedReportsJson => $composableBuilder( + column: $table.supportedReportsJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get currentUserPrivilegesJson => $composableBuilder( + column: $table.currentUserPrivilegesJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get ownerHref => $composableBuilder( + column: $table.ownerHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get safeDisplayMetadataJson => $composableBuilder( + column: $table.safeDisplayMetadataJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get color => $composableBuilder( + column: $table.color, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get calendarTimeZone => $composableBuilder( + column: $table.calendarTimeZone, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get calendarTimeZoneId => $composableBuilder( + column: $table.calendarTimeZoneId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get scheduleTransparency => $composableBuilder( + column: $table.scheduleTransparency, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maximumResourceSize => $composableBuilder( + column: $table.maximumResourceSize, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get maximumInstances => $composableBuilder( + column: $table.maximumInstances, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncToken => $composableBuilder( + column: $table.syncToken, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get ctag => $composableBuilder( + column: $table.ctag, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get readOnly => $composableBuilder( + column: $table.readOnly, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get eventProjectionEnabled => $composableBuilder( + column: $table.eventProjectionEnabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskProjectionEnabled => $composableBuilder( + column: $table.taskProjectionEnabled, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get eventsSelected => $composableBuilder( + column: $table.eventsSelected, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get tasksSelected => $composableBuilder( + column: $table.tasksSelected, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverMissing => $composableBuilder( + column: $table.serverMissing, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deleted => $composableBuilder( + column: $table.deleted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastInventoryAtUtc => $composableBuilder( + column: $table.lastInventoryAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastSyncAtUtc => $composableBuilder( + column: $table.lastSyncAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + $$AccountsTableOrderingComposer get accountId { + final $$AccountsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableOrderingComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavCollectionsTableAnnotationComposer + extends Composer<_$AppDatabase, $DavCollectionsTable> { + $$DavCollectionsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get hrefKey => + $composableBuilder(column: $table.hrefKey, builder: (column) => column); + + GeneratedColumn get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => column, + ); + + GeneratedColumn get displayName => $composableBuilder( + column: $table.displayName, + builder: (column) => column, + ); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, + builder: (column) => column, + ); + + GeneratedColumn get resourceTypesJson => $composableBuilder( + column: $table.resourceTypesJson, + builder: (column) => column, + ); + + GeneratedColumn get supportedComponentMask => $composableBuilder( + column: $table.supportedComponentMask, + builder: (column) => column, + ); + + GeneratedColumn get supportedCalendarDataJson => $composableBuilder( + column: $table.supportedCalendarDataJson, + builder: (column) => column, + ); + + GeneratedColumn get supportedReportsJson => $composableBuilder( + column: $table.supportedReportsJson, + builder: (column) => column, + ); + + GeneratedColumn get currentUserPrivilegesJson => $composableBuilder( + column: $table.currentUserPrivilegesJson, + builder: (column) => column, + ); + + GeneratedColumn get ownerHref => + $composableBuilder(column: $table.ownerHref, builder: (column) => column); + + GeneratedColumn get safeDisplayMetadataJson => $composableBuilder( + column: $table.safeDisplayMetadataJson, + builder: (column) => column, + ); + + GeneratedColumn get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + GeneratedColumn get sortOrder => + $composableBuilder(column: $table.sortOrder, builder: (column) => column); + + GeneratedColumn get calendarTimeZone => $composableBuilder( + column: $table.calendarTimeZone, + builder: (column) => column, + ); + + GeneratedColumn get calendarTimeZoneId => $composableBuilder( + column: $table.calendarTimeZoneId, + builder: (column) => column, + ); + + GeneratedColumn get scheduleTransparency => $composableBuilder( + column: $table.scheduleTransparency, + builder: (column) => column, + ); + + GeneratedColumn get maximumResourceSize => $composableBuilder( + column: $table.maximumResourceSize, + builder: (column) => column, + ); + + GeneratedColumn get maximumInstances => $composableBuilder( + column: $table.maximumInstances, + builder: (column) => column, + ); + + GeneratedColumn get syncToken => + $composableBuilder(column: $table.syncToken, builder: (column) => column); + + GeneratedColumn get ctag => + $composableBuilder(column: $table.ctag, builder: (column) => column); + + GeneratedColumn get readOnly => + $composableBuilder(column: $table.readOnly, builder: (column) => column); + + GeneratedColumn get eventProjectionEnabled => $composableBuilder( + column: $table.eventProjectionEnabled, + builder: (column) => column, + ); + + GeneratedColumn get taskProjectionEnabled => $composableBuilder( + column: $table.taskProjectionEnabled, + builder: (column) => column, + ); + + GeneratedColumn get eventsSelected => $composableBuilder( + column: $table.eventsSelected, + builder: (column) => column, + ); + + GeneratedColumn get tasksSelected => $composableBuilder( + column: $table.tasksSelected, + builder: (column) => column, + ); + + GeneratedColumn get serverMissing => $composableBuilder( + column: $table.serverMissing, + builder: (column) => column, + ); + + GeneratedColumn get deleted => + $composableBuilder(column: $table.deleted, builder: (column) => column); + + GeneratedColumn get lastInventoryAtUtc => $composableBuilder( + column: $table.lastInventoryAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastSyncAtUtc => $composableBuilder( + column: $table.lastSyncAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => column, + ); + + GeneratedColumn get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => column, + ); + + GeneratedColumn get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get updatedAtUtc => $composableBuilder( + column: $table.updatedAtUtc, + builder: (column) => column, + ); + + $$AccountsTableAnnotationComposer get accountId { + final $$AccountsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableAnnotationComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression davObjectsRefs( + Expression Function($$DavObjectsTableAnnotationComposer a) f, + ) { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.collectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableAnnotationComposer a) f, + ) { + final $$DavConflictSnapshotsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableAnnotationComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression taskListsRefs( + Expression Function($$TaskListsTableAnnotationComposer a) f, + ) { + final $$TaskListsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.taskLists, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TaskListsTableAnnotationComposer( + $db: $db, + $table: $db.taskLists, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableAnnotationComposer a) f, + ) { + final $$TasksTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableAnnotationComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarSourcesRefs( + Expression Function($$CalendarSourcesTableAnnotationComposer a) f, + ) { + final $$CalendarSourcesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarSources, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarSourcesTableAnnotationComposer( + $db: $db, + $table: $db.calendarSources, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableAnnotationComposer a) f, + ) { + final $$CalendarEventsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableAnnotationComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableAnnotationComposer a) f, + ) { + final $$SyncCursorsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.davCollectionId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncCursorsTableAnnotationComposer( + $db: $db, + $table: $db.syncCursors, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavCollectionsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DavCollectionsTable, + DavCollection, + $$DavCollectionsTableFilterComposer, + $$DavCollectionsTableOrderingComposer, + $$DavCollectionsTableAnnotationComposer, + $$DavCollectionsTableCreateCompanionBuilder, + $$DavCollectionsTableUpdateCompanionBuilder, + (DavCollection, $$DavCollectionsTableReferences), + DavCollection, + PrefetchHooks Function({ + bool accountId, + bool davObjectsRefs, + bool davConflictSnapshotsRefs, + bool taskListsRefs, + bool tasksRefs, + bool calendarSourcesRefs, + bool calendarEventsRefs, + bool syncCursorsRefs, + }) + > { + $$DavCollectionsTableTableManager( + _$AppDatabase db, + $DavCollectionsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DavCollectionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DavCollectionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DavCollectionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value accountId = const Value.absent(), + Value hrefKey = const Value.absent(), + Value requestUri = const Value.absent(), + Value displayName = const Value.absent(), + Value description = const Value.absent(), + Value resourceTypesJson = const Value.absent(), + Value supportedComponentMask = const Value.absent(), + Value supportedCalendarDataJson = const Value.absent(), + Value supportedReportsJson = const Value.absent(), + Value currentUserPrivilegesJson = const Value.absent(), + Value ownerHref = const Value.absent(), + Value safeDisplayMetadataJson = const Value.absent(), + Value color = const Value.absent(), + Value sortOrder = const Value.absent(), + Value calendarTimeZone = const Value.absent(), + Value calendarTimeZoneId = const Value.absent(), + Value scheduleTransparency = const Value.absent(), + Value maximumResourceSize = const Value.absent(), + Value maximumInstances = const Value.absent(), + Value syncToken = const Value.absent(), + Value ctag = const Value.absent(), + Value readOnly = const Value.absent(), + Value eventProjectionEnabled = const Value.absent(), + Value taskProjectionEnabled = const Value.absent(), + Value eventsSelected = const Value.absent(), + Value tasksSelected = const Value.absent(), + Value serverMissing = const Value.absent(), + Value deleted = const Value.absent(), + Value lastInventoryAtUtc = const Value.absent(), + Value lastSyncAtUtc = const Value.absent(), + Value parserVersion = const Value.absent(), + Value projectionVersion = const Value.absent(), + Value createdAtUtc = const Value.absent(), + Value updatedAtUtc = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavCollectionsCompanion( + id: id, + accountId: accountId, + hrefKey: hrefKey, + requestUri: requestUri, + displayName: displayName, + description: description, + resourceTypesJson: resourceTypesJson, + supportedComponentMask: supportedComponentMask, + supportedCalendarDataJson: supportedCalendarDataJson, + supportedReportsJson: supportedReportsJson, + currentUserPrivilegesJson: currentUserPrivilegesJson, + ownerHref: ownerHref, + safeDisplayMetadataJson: safeDisplayMetadataJson, + color: color, + sortOrder: sortOrder, + calendarTimeZone: calendarTimeZone, + calendarTimeZoneId: calendarTimeZoneId, + scheduleTransparency: scheduleTransparency, + maximumResourceSize: maximumResourceSize, + maximumInstances: maximumInstances, + syncToken: syncToken, + ctag: ctag, + readOnly: readOnly, + eventProjectionEnabled: eventProjectionEnabled, + taskProjectionEnabled: taskProjectionEnabled, + eventsSelected: eventsSelected, + tasksSelected: tasksSelected, + serverMissing: serverMissing, + deleted: deleted, + lastInventoryAtUtc: lastInventoryAtUtc, + lastSyncAtUtc: lastSyncAtUtc, + parserVersion: parserVersion, + projectionVersion: projectionVersion, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String accountId, + required String hrefKey, + required String requestUri, + required String displayName, + Value description = const Value.absent(), + Value resourceTypesJson = const Value.absent(), + Value supportedComponentMask = const Value.absent(), + Value supportedCalendarDataJson = const Value.absent(), + Value supportedReportsJson = const Value.absent(), + Value currentUserPrivilegesJson = const Value.absent(), + Value ownerHref = const Value.absent(), + Value safeDisplayMetadataJson = const Value.absent(), + Value color = const Value.absent(), + Value sortOrder = const Value.absent(), + Value calendarTimeZone = const Value.absent(), + Value calendarTimeZoneId = const Value.absent(), + Value scheduleTransparency = const Value.absent(), + Value maximumResourceSize = const Value.absent(), + Value maximumInstances = const Value.absent(), + Value syncToken = const Value.absent(), + Value ctag = const Value.absent(), + Value readOnly = const Value.absent(), + Value eventProjectionEnabled = const Value.absent(), + Value taskProjectionEnabled = const Value.absent(), + Value eventsSelected = const Value.absent(), + Value tasksSelected = const Value.absent(), + Value serverMissing = const Value.absent(), + Value deleted = const Value.absent(), + Value lastInventoryAtUtc = const Value.absent(), + Value lastSyncAtUtc = const Value.absent(), + Value parserVersion = const Value.absent(), + Value projectionVersion = const Value.absent(), + required String createdAtUtc, + required String updatedAtUtc, + Value rowid = const Value.absent(), + }) => DavCollectionsCompanion.insert( + id: id, + accountId: accountId, + hrefKey: hrefKey, + requestUri: requestUri, + displayName: displayName, + description: description, + resourceTypesJson: resourceTypesJson, + supportedComponentMask: supportedComponentMask, + supportedCalendarDataJson: supportedCalendarDataJson, + supportedReportsJson: supportedReportsJson, + currentUserPrivilegesJson: currentUserPrivilegesJson, + ownerHref: ownerHref, + safeDisplayMetadataJson: safeDisplayMetadataJson, + color: color, + sortOrder: sortOrder, + calendarTimeZone: calendarTimeZone, + calendarTimeZoneId: calendarTimeZoneId, + scheduleTransparency: scheduleTransparency, + maximumResourceSize: maximumResourceSize, + maximumInstances: maximumInstances, + syncToken: syncToken, + ctag: ctag, + readOnly: readOnly, + eventProjectionEnabled: eventProjectionEnabled, + taskProjectionEnabled: taskProjectionEnabled, + eventsSelected: eventsSelected, + tasksSelected: tasksSelected, + serverMissing: serverMissing, + deleted: deleted, + lastInventoryAtUtc: lastInventoryAtUtc, + lastSyncAtUtc: lastSyncAtUtc, + parserVersion: parserVersion, + projectionVersion: projectionVersion, + createdAtUtc: createdAtUtc, + updatedAtUtc: updatedAtUtc, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$DavCollectionsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + accountId = false, + davObjectsRefs = false, + davConflictSnapshotsRefs = false, + taskListsRefs = false, + tasksRefs = false, + calendarSourcesRefs = false, + calendarEventsRefs = false, + syncCursorsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (davObjectsRefs) db.davObjects, + if (davConflictSnapshotsRefs) db.davConflictSnapshots, + if (taskListsRefs) db.taskLists, + if (tasksRefs) db.tasks, + if (calendarSourcesRefs) db.calendarSources, + if (calendarEventsRefs) db.calendarEvents, + if (syncCursorsRefs) db.syncCursors, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: + $$DavCollectionsTableReferences + ._accountIdTable(db), + referencedColumn: + $$DavCollectionsTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (davObjectsRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + DavObject + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._davObjectsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).davObjectsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.collectionId == item.id, + ), + typedResults: items, + ), + if (davConflictSnapshotsRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + DavConflictSnapshot + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._davConflictSnapshotsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).davConflictSnapshotsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + if (taskListsRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + TaskList + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._taskListsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).taskListsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + if (tasksRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + Task + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._tasksRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).tasksRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + if (calendarSourcesRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + CalendarSource + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._calendarSourcesRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).calendarSourcesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + if (calendarEventsRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + CalendarEvent + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._calendarEventsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).calendarEventsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + if (syncCursorsRefs) + await $_getPrefetchedData< + DavCollection, + $DavCollectionsTable, + SyncCursor + >( + currentTable: table, + referencedTable: $$DavCollectionsTableReferences + ._syncCursorsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavCollectionsTableReferences( + db, + table, + p0, + ).syncCursorsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davCollectionId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$DavCollectionsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DavCollectionsTable, + DavCollection, + $$DavCollectionsTableFilterComposer, + $$DavCollectionsTableOrderingComposer, + $$DavCollectionsTableAnnotationComposer, + $$DavCollectionsTableCreateCompanionBuilder, + $$DavCollectionsTableUpdateCompanionBuilder, + (DavCollection, $$DavCollectionsTableReferences), + DavCollection, + PrefetchHooks Function({ + bool accountId, + bool davObjectsRefs, + bool davConflictSnapshotsRefs, + bool taskListsRefs, + bool tasksRefs, + bool calendarSourcesRefs, + bool calendarEventsRefs, + bool syncCursorsRefs, + }) + >; +typedef $$DavObjectsTableCreateCompanionBuilder = + DavObjectsCompanion Function({ + required String id, + required String accountId, + required String collectionId, + required String hrefKey, + required String requestUri, + Value etag, + Value contentType, + Value dominantComponentType, + Value componentMask, + Value primaryUid, + required String rawIcsBody, + required String rawBodyHash, + Value semanticHash, + Value serverDeleted, + Value baselineGeneration, + required String firstSeenAtUtc, + required String lastFetchedAtUtc, + required String lastChangedAtUtc, + Value lastParseStatus, + Value lastParseErrorCode, + Value parserVersion, + Value rowid, + }); +typedef $$DavObjectsTableUpdateCompanionBuilder = + DavObjectsCompanion Function({ + Value id, + Value accountId, + Value collectionId, + Value hrefKey, + Value requestUri, + Value etag, + Value contentType, + Value dominantComponentType, + Value componentMask, + Value primaryUid, + Value rawIcsBody, + Value rawBodyHash, + Value semanticHash, + Value serverDeleted, + Value baselineGeneration, + Value firstSeenAtUtc, + Value lastFetchedAtUtc, + Value lastChangedAtUtc, + Value lastParseStatus, + Value lastParseErrorCode, + Value parserVersion, + Value rowid, + }); + +final class $$DavObjectsTableReferences + extends BaseReferences<_$AppDatabase, $DavObjectsTable, DavObject> { + $$DavObjectsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static $AccountsTable _accountIdTable(_$AppDatabase db) => + db.accounts.createAlias( + $_aliasNameGenerator(db.davObjects.accountId, db.accounts.id), + ); + + $$AccountsTableProcessedTableManager get accountId { + final $_column = $_itemColumn('account_id')!; + + final manager = $$AccountsTableTableManager( + $_db, + $_db.accounts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavCollectionsTable _collectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator(db.davObjects.collectionId, db.davCollections.id), + ); + + $$DavCollectionsTableProcessedTableManager get collectionId { + final $_column = $_itemColumn('collection_id')!; + + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_collectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey< + $DavObjectComponentsTable, + List + > + _davObjectComponentsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.davObjectComponents, + aliasName: $_aliasNameGenerator( + db.davObjects.id, + db.davObjectComponents.davObjectId, + ), + ); + + $$DavObjectComponentsTableProcessedTableManager get davObjectComponentsRefs { + final manager = $$DavObjectComponentsTableTableManager( + $_db, + $_db.davObjectComponents, + ).filter((f) => f.davObjectId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _davObjectComponentsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $DavConflictSnapshotsTable, + List + > + _davConflictSnapshotsRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.davConflictSnapshots, + aliasName: $_aliasNameGenerator( + db.davObjects.id, + db.davConflictSnapshots.davObjectId, + ), + ); + + $$DavConflictSnapshotsTableProcessedTableManager + get davConflictSnapshotsRefs { + final manager = $$DavConflictSnapshotsTableTableManager( + $_db, + $_db.davConflictSnapshots, + ).filter((f) => f.davObjectId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _davConflictSnapshotsRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$TasksTable, List> _tasksRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.tasks, + aliasName: $_aliasNameGenerator(db.davObjects.id, db.tasks.davObjectId), + ); + + $$TasksTableProcessedTableManager get tasksRefs { + final manager = $$TasksTableTableManager( + $_db, + $_db.tasks, + ).filter((f) => f.davObjectId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_tasksRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PendingOpsTable, List> + _pendingOpsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.pendingOps, + aliasName: $_aliasNameGenerator( + db.davObjects.id, + db.pendingOps.davObjectId, + ), + ); + + $$PendingOpsTableProcessedTableManager get pendingOpsRefs { + final manager = $$PendingOpsTableTableManager( + $_db, + $_db.pendingOps, + ).filter((f) => f.davObjectId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_pendingOpsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarEventsTable, List> + _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarEvents, + aliasName: $_aliasNameGenerator( + db.davObjects.id, + db.calendarEvents.davObjectId, + ), + ); + + $$CalendarEventsTableProcessedTableManager get calendarEventsRefs { + final manager = $$CalendarEventsTableTableManager( + $_db, + $_db.calendarEvents, + ).filter((f) => f.davObjectId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_calendarEventsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$DavObjectsTableFilterComposer + extends Composer<_$AppDatabase, $DavObjectsTable> { + $$DavObjectsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get hrefKey => $composableBuilder( + column: $table.hrefKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get etag => $composableBuilder( + column: $table.etag, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get contentType => $composableBuilder( + column: $table.contentType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get dominantComponentType => $composableBuilder( + column: $table.dominantComponentType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get componentMask => $composableBuilder( + column: $table.componentMask, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get primaryUid => $composableBuilder( + column: $table.primaryUid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rawIcsBody => $composableBuilder( + column: $table.rawIcsBody, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get rawBodyHash => $composableBuilder( + column: $table.rawBodyHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get serverDeleted => $composableBuilder( + column: $table.serverDeleted, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get firstSeenAtUtc => $composableBuilder( + column: $table.firstSeenAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastFetchedAtUtc => $composableBuilder( + column: $table.lastFetchedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastChangedAtUtc => $composableBuilder( + column: $table.lastChangedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastParseStatus => $composableBuilder( + column: $table.lastParseStatus, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastParseErrorCode => $composableBuilder( + column: $table.lastParseErrorCode, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => ColumnFilters(column), + ); + + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableFilterComposer get collectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.collectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression davObjectComponentsRefs( + Expression Function($$DavObjectComponentsTableFilterComposer f) f, + ) { + final $$DavObjectComponentsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableFilterComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableFilterComposer f) f, + ) { + final $$DavConflictSnapshotsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableFilterComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableFilterComposer f) f, + ) { + final $$TasksTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableFilterComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableFilterComposer f) f, + ) { + final $$PendingOpsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableFilterComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableFilterComposer f) f, + ) { + final $$CalendarEventsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableFilterComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavObjectsTableOrderingComposer + extends Composer<_$AppDatabase, $DavObjectsTable> { + $$DavObjectsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get hrefKey => $composableBuilder( + column: $table.hrefKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get etag => $composableBuilder( + column: $table.etag, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get contentType => $composableBuilder( + column: $table.contentType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get dominantComponentType => $composableBuilder( + column: $table.dominantComponentType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get componentMask => $composableBuilder( + column: $table.componentMask, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get primaryUid => $composableBuilder( + column: $table.primaryUid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rawIcsBody => $composableBuilder( + column: $table.rawIcsBody, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get rawBodyHash => $composableBuilder( + column: $table.rawBodyHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get serverDeleted => $composableBuilder( + column: $table.serverDeleted, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get firstSeenAtUtc => $composableBuilder( + column: $table.firstSeenAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastFetchedAtUtc => $composableBuilder( + column: $table.lastFetchedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastChangedAtUtc => $composableBuilder( + column: $table.lastChangedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastParseStatus => $composableBuilder( + column: $table.lastParseStatus, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastParseErrorCode => $composableBuilder( + column: $table.lastParseErrorCode, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => ColumnOrderings(column), + ); + + $$AccountsTableOrderingComposer get accountId { + final $$AccountsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableOrderingComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableOrderingComposer get collectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.collectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavObjectsTableAnnotationComposer + extends Composer<_$AppDatabase, $DavObjectsTable> { + $$DavObjectsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get hrefKey => + $composableBuilder(column: $table.hrefKey, builder: (column) => column); + + GeneratedColumn get requestUri => $composableBuilder( + column: $table.requestUri, + builder: (column) => column, + ); + + GeneratedColumn get etag => + $composableBuilder(column: $table.etag, builder: (column) => column); + + GeneratedColumn get contentType => $composableBuilder( + column: $table.contentType, + builder: (column) => column, + ); + + GeneratedColumn get dominantComponentType => $composableBuilder( + column: $table.dominantComponentType, + builder: (column) => column, + ); + + GeneratedColumn get componentMask => $composableBuilder( + column: $table.componentMask, + builder: (column) => column, + ); + + GeneratedColumn get primaryUid => $composableBuilder( + column: $table.primaryUid, + builder: (column) => column, + ); + + GeneratedColumn get rawIcsBody => $composableBuilder( + column: $table.rawIcsBody, + builder: (column) => column, + ); + + GeneratedColumn get rawBodyHash => $composableBuilder( + column: $table.rawBodyHash, + builder: (column) => column, + ); + + GeneratedColumn get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => column, + ); + + GeneratedColumn get serverDeleted => $composableBuilder( + column: $table.serverDeleted, + builder: (column) => column, + ); + + GeneratedColumn get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, + builder: (column) => column, + ); + + GeneratedColumn get firstSeenAtUtc => $composableBuilder( + column: $table.firstSeenAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastFetchedAtUtc => $composableBuilder( + column: $table.lastFetchedAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastChangedAtUtc => $composableBuilder( + column: $table.lastChangedAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastParseStatus => $composableBuilder( + column: $table.lastParseStatus, + builder: (column) => column, + ); + + GeneratedColumn get lastParseErrorCode => $composableBuilder( + column: $table.lastParseErrorCode, + builder: (column) => column, + ); + + GeneratedColumn get parserVersion => $composableBuilder( + column: $table.parserVersion, + builder: (column) => column, + ); + + $$AccountsTableAnnotationComposer get accountId { + final $$AccountsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableAnnotationComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableAnnotationComposer get collectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.collectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression davObjectComponentsRefs( + Expression Function($$DavObjectComponentsTableAnnotationComposer a) f, + ) { + final $$DavObjectComponentsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableAnnotationComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression davConflictSnapshotsRefs( + Expression Function($$DavConflictSnapshotsTableAnnotationComposer a) f, + ) { + final $$DavConflictSnapshotsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableAnnotationComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression tasksRefs( + Expression Function($$TasksTableAnnotationComposer a) f, + ) { + final $$TasksTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableAnnotationComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableAnnotationComposer a) f, + ) { + final $$PendingOpsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableAnnotationComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableAnnotationComposer a) f, + ) { + final $$CalendarEventsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davObjectId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableAnnotationComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavObjectsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DavObjectsTable, + DavObject, + $$DavObjectsTableFilterComposer, + $$DavObjectsTableOrderingComposer, + $$DavObjectsTableAnnotationComposer, + $$DavObjectsTableCreateCompanionBuilder, + $$DavObjectsTableUpdateCompanionBuilder, + (DavObject, $$DavObjectsTableReferences), + DavObject, + PrefetchHooks Function({ + bool accountId, + bool collectionId, + bool davObjectComponentsRefs, + bool davConflictSnapshotsRefs, + bool tasksRefs, + bool pendingOpsRefs, + bool calendarEventsRefs, + }) + > { + $$DavObjectsTableTableManager(_$AppDatabase db, $DavObjectsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DavObjectsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DavObjectsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DavObjectsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value accountId = const Value.absent(), + Value collectionId = const Value.absent(), + Value hrefKey = const Value.absent(), + Value requestUri = const Value.absent(), + Value etag = const Value.absent(), + Value contentType = const Value.absent(), + Value dominantComponentType = const Value.absent(), + Value componentMask = const Value.absent(), + Value primaryUid = const Value.absent(), + Value rawIcsBody = const Value.absent(), + Value rawBodyHash = const Value.absent(), + Value semanticHash = const Value.absent(), + Value serverDeleted = const Value.absent(), + Value baselineGeneration = const Value.absent(), + Value firstSeenAtUtc = const Value.absent(), + Value lastFetchedAtUtc = const Value.absent(), + Value lastChangedAtUtc = const Value.absent(), + Value lastParseStatus = const Value.absent(), + Value lastParseErrorCode = const Value.absent(), + Value parserVersion = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavObjectsCompanion( + id: id, + accountId: accountId, + collectionId: collectionId, + hrefKey: hrefKey, + requestUri: requestUri, + etag: etag, + contentType: contentType, + dominantComponentType: dominantComponentType, + componentMask: componentMask, + primaryUid: primaryUid, + rawIcsBody: rawIcsBody, + rawBodyHash: rawBodyHash, + semanticHash: semanticHash, + serverDeleted: serverDeleted, + baselineGeneration: baselineGeneration, + firstSeenAtUtc: firstSeenAtUtc, + lastFetchedAtUtc: lastFetchedAtUtc, + lastChangedAtUtc: lastChangedAtUtc, + lastParseStatus: lastParseStatus, + lastParseErrorCode: lastParseErrorCode, + parserVersion: parserVersion, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String accountId, + required String collectionId, + required String hrefKey, + required String requestUri, + Value etag = const Value.absent(), + Value contentType = const Value.absent(), + Value dominantComponentType = const Value.absent(), + Value componentMask = const Value.absent(), + Value primaryUid = const Value.absent(), + required String rawIcsBody, + required String rawBodyHash, + Value semanticHash = const Value.absent(), + Value serverDeleted = const Value.absent(), + Value baselineGeneration = const Value.absent(), + required String firstSeenAtUtc, + required String lastFetchedAtUtc, + required String lastChangedAtUtc, + Value lastParseStatus = const Value.absent(), + Value lastParseErrorCode = const Value.absent(), + Value parserVersion = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavObjectsCompanion.insert( + id: id, + accountId: accountId, + collectionId: collectionId, + hrefKey: hrefKey, + requestUri: requestUri, + etag: etag, + contentType: contentType, + dominantComponentType: dominantComponentType, + componentMask: componentMask, + primaryUid: primaryUid, + rawIcsBody: rawIcsBody, + rawBodyHash: rawBodyHash, + semanticHash: semanticHash, + serverDeleted: serverDeleted, + baselineGeneration: baselineGeneration, + firstSeenAtUtc: firstSeenAtUtc, + lastFetchedAtUtc: lastFetchedAtUtc, + lastChangedAtUtc: lastChangedAtUtc, + lastParseStatus: lastParseStatus, + lastParseErrorCode: lastParseErrorCode, + parserVersion: parserVersion, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$DavObjectsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + accountId = false, + collectionId = false, + davObjectComponentsRefs = false, + davConflictSnapshotsRefs = false, + tasksRefs = false, + pendingOpsRefs = false, + calendarEventsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (davObjectComponentsRefs) db.davObjectComponents, + if (davConflictSnapshotsRefs) db.davConflictSnapshots, + if (tasksRefs) db.tasks, + if (pendingOpsRefs) db.pendingOps, + if (calendarEventsRefs) db.calendarEvents, + ], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: $$DavObjectsTableReferences + ._accountIdTable(db), + referencedColumn: + $$DavObjectsTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + if (collectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.collectionId, + referencedTable: $$DavObjectsTableReferences + ._collectionIdTable(db), + referencedColumn: + $$DavObjectsTableReferences + ._collectionIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (davObjectComponentsRefs) + await $_getPrefetchedData< + DavObject, + $DavObjectsTable, + DavObjectComponent + >( + currentTable: table, + referencedTable: $$DavObjectsTableReferences + ._davObjectComponentsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavObjectsTableReferences( + db, + table, + p0, + ).davObjectComponentsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davObjectId == item.id, + ), + typedResults: items, + ), + if (davConflictSnapshotsRefs) + await $_getPrefetchedData< + DavObject, + $DavObjectsTable, + DavConflictSnapshot + >( + currentTable: table, + referencedTable: $$DavObjectsTableReferences + ._davConflictSnapshotsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavObjectsTableReferences( + db, + table, + p0, + ).davConflictSnapshotsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davObjectId == item.id, + ), + typedResults: items, + ), + if (tasksRefs) + await $_getPrefetchedData< + DavObject, + $DavObjectsTable, + Task + >( + currentTable: table, + referencedTable: $$DavObjectsTableReferences + ._tasksRefsTable(db), + managerFromTypedResult: (p0) => + $$DavObjectsTableReferences( + db, + table, + p0, + ).tasksRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davObjectId == item.id, + ), + typedResults: items, + ), + if (pendingOpsRefs) + await $_getPrefetchedData< + DavObject, + $DavObjectsTable, + PendingOp + >( + currentTable: table, + referencedTable: $$DavObjectsTableReferences + ._pendingOpsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavObjectsTableReferences( + db, + table, + p0, + ).pendingOpsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davObjectId == item.id, + ), + typedResults: items, + ), + if (calendarEventsRefs) + await $_getPrefetchedData< + DavObject, + $DavObjectsTable, + CalendarEvent + >( + currentTable: table, + referencedTable: $$DavObjectsTableReferences + ._calendarEventsRefsTable(db), + managerFromTypedResult: (p0) => + $$DavObjectsTableReferences( + db, + table, + p0, + ).calendarEventsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.davObjectId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$DavObjectsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DavObjectsTable, + DavObject, + $$DavObjectsTableFilterComposer, + $$DavObjectsTableOrderingComposer, + $$DavObjectsTableAnnotationComposer, + $$DavObjectsTableCreateCompanionBuilder, + $$DavObjectsTableUpdateCompanionBuilder, + (DavObject, $$DavObjectsTableReferences), + DavObject, + PrefetchHooks Function({ + bool accountId, + bool collectionId, + bool davObjectComponentsRefs, + bool davConflictSnapshotsRefs, + bool tasksRefs, + bool pendingOpsRefs, + bool calendarEventsRefs, + }) + >; +typedef $$DavObjectComponentsTableCreateCompanionBuilder = + DavObjectComponentsCompanion Function({ + required String id, + required String davObjectId, + required String componentType, + required String uid, + Value recurrenceIdKey, + Value sequence, + Value dtstampUtc, + Value lastModifiedUtc, + required String semanticHash, + Value parserProfileVersion, + Value rowid, + }); +typedef $$DavObjectComponentsTableUpdateCompanionBuilder = + DavObjectComponentsCompanion Function({ + Value id, + Value davObjectId, + Value componentType, + Value uid, + Value recurrenceIdKey, + Value sequence, + Value dtstampUtc, + Value lastModifiedUtc, + Value semanticHash, + Value parserProfileVersion, + Value rowid, + }); + +final class $$DavObjectComponentsTableReferences + extends + BaseReferences< + _$AppDatabase, + $DavObjectComponentsTable, + DavObjectComponent + > { + $$DavObjectComponentsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $DavObjectsTable _davObjectIdTable(_$AppDatabase db) => + db.davObjects.createAlias( + $_aliasNameGenerator( + db.davObjectComponents.davObjectId, + db.davObjects.id, + ), + ); + + $$DavObjectsTableProcessedTableManager get davObjectId { + final $_column = $_itemColumn('dav_object_id')!; + + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davObjectIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$TasksTable, List> _tasksRefsTable( + _$AppDatabase db, + ) => MultiTypedResultKey.fromTable( + db.tasks, + aliasName: $_aliasNameGenerator( + db.davObjectComponents.id, + db.tasks.davComponentId, + ), + ); + + $$TasksTableProcessedTableManager get tasksRefs { + final manager = $$TasksTableTableManager( + $_db, + $_db.tasks, + ).filter((f) => f.davComponentId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_tasksRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$CalendarEventsTable, List> + _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.calendarEvents, + aliasName: $_aliasNameGenerator( + db.davObjectComponents.id, + db.calendarEvents.davComponentId, + ), + ); + + $$CalendarEventsTableProcessedTableManager get calendarEventsRefs { + final manager = $$CalendarEventsTableTableManager( + $_db, + $_db.calendarEvents, + ).filter((f) => f.davComponentId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_calendarEventsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$DavObjectComponentsTableFilterComposer + extends Composer<_$AppDatabase, $DavObjectComponentsTable> { + $$DavObjectComponentsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get componentType => $composableBuilder( + column: $table.componentType, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get uid => $composableBuilder( + column: $table.uid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sequence => $composableBuilder( + column: $table.sequence, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get dtstampUtc => $composableBuilder( + column: $table.dtstampUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get lastModifiedUtc => $composableBuilder( + column: $table.lastModifiedUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get parserProfileVersion => $composableBuilder( + column: $table.parserProfileVersion, + builder: (column) => ColumnFilters(column), + ); + + $$DavObjectsTableFilterComposer get davObjectId { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => $$CalendarEventsTableAnnotationComposer( + }) => $$DavObjectsTableFilterComposer( $db: $db, - $table: $db.calendarEvents, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression tasksRefs( + Expression Function($$TasksTableFilterComposer f) f, + ) { + final $$TasksTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davComponentId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableFilterComposer( + $db: $db, + $table: $db.tasks, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -14990,419 +28297,1218 @@ class $$AccountsTableAnnotationComposer return f(composer); } - Expression calendarSyncStatesRefs( - Expression Function($$CalendarSyncStatesTableAnnotationComposer a) f, + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableFilterComposer f) f, ) { - final $$CalendarSyncStatesTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSyncStates, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, + final $$CalendarEventsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davComponentId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableFilterComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, - }) => $$CalendarSyncStatesTableAnnotationComposer( - $db: $db, - $table: $db.calendarSyncStates, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); + ), + ); return f(composer); } +} - Expression scheduleItemOverridesRefs( - Expression Function($$ScheduleItemOverridesTableAnnotationComposer a) f, +class $$DavObjectComponentsTableOrderingComposer + extends Composer<_$AppDatabase, $DavObjectComponentsTable> { + $$DavObjectComponentsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get componentType => $composableBuilder( + column: $table.componentType, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get uid => $composableBuilder( + column: $table.uid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sequence => $composableBuilder( + column: $table.sequence, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get dtstampUtc => $composableBuilder( + column: $table.dtstampUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get lastModifiedUtc => $composableBuilder( + column: $table.lastModifiedUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get parserProfileVersion => $composableBuilder( + column: $table.parserProfileVersion, + builder: (column) => ColumnOrderings(column), + ); + + $$DavObjectsTableOrderingComposer get davObjectId { + final $$DavObjectsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableOrderingComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavObjectComponentsTableAnnotationComposer + extends Composer<_$AppDatabase, $DavObjectComponentsTable> { + $$DavObjectComponentsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get componentType => $composableBuilder( + column: $table.componentType, + builder: (column) => column, + ); + + GeneratedColumn get uid => + $composableBuilder(column: $table.uid, builder: (column) => column); + + GeneratedColumn get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => column, + ); + + GeneratedColumn get sequence => + $composableBuilder(column: $table.sequence, builder: (column) => column); + + GeneratedColumn get dtstampUtc => $composableBuilder( + column: $table.dtstampUtc, + builder: (column) => column, + ); + + GeneratedColumn get lastModifiedUtc => $composableBuilder( + column: $table.lastModifiedUtc, + builder: (column) => column, + ); + + GeneratedColumn get semanticHash => $composableBuilder( + column: $table.semanticHash, + builder: (column) => column, + ); + + GeneratedColumn get parserProfileVersion => $composableBuilder( + column: $table.parserProfileVersion, + builder: (column) => column, + ); + + $$DavObjectsTableAnnotationComposer get davObjectId { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression tasksRefs( + Expression Function($$TasksTableAnnotationComposer a) f, ) { - final $$ScheduleItemOverridesTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.scheduleItemOverrides, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, + final $$TasksTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.tasks, + getReferencedColumn: (t) => t.davComponentId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$TasksTableAnnotationComposer( + $db: $db, + $table: $db.tasks, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, - }) => $$ScheduleItemOverridesTableAnnotationComposer( - $db: $db, - $table: $db.scheduleItemOverrides, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); + ), + ); return f(composer); } - Expression notificationScheduleRefs( - Expression Function($$NotificationScheduleTableAnnotationComposer a) f, + Expression calendarEventsRefs( + Expression Function($$CalendarEventsTableAnnotationComposer a) f, ) { - final $$NotificationScheduleTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.notificationSchedule, - getReferencedColumn: (t) => t.accountId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, + final $$CalendarEventsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.calendarEvents, + getReferencedColumn: (t) => t.davComponentId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$CalendarEventsTableAnnotationComposer( + $db: $db, + $table: $db.calendarEvents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, - }) => $$NotificationScheduleTableAnnotationComposer( - $db: $db, - $table: $db.notificationSchedule, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); + ), + ); return f(composer); } } -class $$AccountsTableTableManager +class $$DavObjectComponentsTableTableManager extends RootTableManager< _$AppDatabase, - $AccountsTable, - Account, - $$AccountsTableFilterComposer, - $$AccountsTableOrderingComposer, - $$AccountsTableAnnotationComposer, - $$AccountsTableCreateCompanionBuilder, - $$AccountsTableUpdateCompanionBuilder, - (Account, $$AccountsTableReferences), - Account, + $DavObjectComponentsTable, + DavObjectComponent, + $$DavObjectComponentsTableFilterComposer, + $$DavObjectComponentsTableOrderingComposer, + $$DavObjectComponentsTableAnnotationComposer, + $$DavObjectComponentsTableCreateCompanionBuilder, + $$DavObjectComponentsTableUpdateCompanionBuilder, + (DavObjectComponent, $$DavObjectComponentsTableReferences), + DavObjectComponent, PrefetchHooks Function({ - bool taskListsRefs, + bool davObjectId, bool tasksRefs, - bool pendingOpsRefs, - bool syncRunsRefs, - bool calendarSourcesRefs, bool calendarEventsRefs, - bool calendarSyncStatesRefs, - bool scheduleItemOverridesRefs, - bool notificationScheduleRefs, }) > { - $$AccountsTableTableManager(_$AppDatabase db, $AccountsTable table) - : super( + $$DavObjectComponentsTableTableManager( + _$AppDatabase db, + $DavObjectComponentsTable table, + ) : super( TableManagerState( db: db, table: table, createFilteringComposer: () => - $$AccountsTableFilterComposer($db: db, $table: table), + $$DavObjectComponentsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$AccountsTableOrderingComposer($db: db, $table: table), + $$DavObjectComponentsTableOrderingComposer( + $db: db, + $table: table, + ), createComputedFieldComposer: () => - $$AccountsTableAnnotationComposer($db: db, $table: table), + $$DavObjectComponentsTableAnnotationComposer( + $db: db, + $table: table, + ), updateCompanionCallback: ({ Value id = const Value.absent(), - Value provider = const Value.absent(), - Value providerAccountId = const Value.absent(), - Value displayName = const Value.absent(), - Value email = const Value.absent(), - Value tenantId = const Value.absent(), - Value accountAvatarUrl = const Value.absent(), - Value providerMetadataJson = const Value.absent(), - Value authState = const Value.absent(), - Value calendarsEnabled = const Value.absent(), - Value tasksEnabled = const Value.absent(), - Value grantedScopes = const Value.absent(), - Value createdAtUtc = const Value.absent(), - Value updatedAtUtc = const Value.absent(), - Value lastSuccessfulSyncAtUtc = const Value.absent(), - Value lastFullSyncAtUtc = const Value.absent(), + Value davObjectId = const Value.absent(), + Value componentType = const Value.absent(), + Value uid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value sequence = const Value.absent(), + Value dtstampUtc = const Value.absent(), + Value lastModifiedUtc = const Value.absent(), + Value semanticHash = const Value.absent(), + Value parserProfileVersion = const Value.absent(), Value rowid = const Value.absent(), - }) => AccountsCompanion( + }) => DavObjectComponentsCompanion( id: id, - provider: provider, - providerAccountId: providerAccountId, - displayName: displayName, - email: email, - tenantId: tenantId, - accountAvatarUrl: accountAvatarUrl, - providerMetadataJson: providerMetadataJson, - authState: authState, - calendarsEnabled: calendarsEnabled, - tasksEnabled: tasksEnabled, - grantedScopes: grantedScopes, - createdAtUtc: createdAtUtc, - updatedAtUtc: updatedAtUtc, - lastSuccessfulSyncAtUtc: lastSuccessfulSyncAtUtc, - lastFullSyncAtUtc: lastFullSyncAtUtc, + davObjectId: davObjectId, + componentType: componentType, + uid: uid, + recurrenceIdKey: recurrenceIdKey, + sequence: sequence, + dtstampUtc: dtstampUtc, + lastModifiedUtc: lastModifiedUtc, + semanticHash: semanticHash, + parserProfileVersion: parserProfileVersion, rowid: rowid, ), createCompanionCallback: ({ required String id, - Value provider = const Value.absent(), - Value providerAccountId = const Value.absent(), - Value displayName = const Value.absent(), - Value email = const Value.absent(), - Value tenantId = const Value.absent(), - Value accountAvatarUrl = const Value.absent(), - Value providerMetadataJson = const Value.absent(), - Value authState = const Value.absent(), - Value calendarsEnabled = const Value.absent(), - Value tasksEnabled = const Value.absent(), - Value grantedScopes = const Value.absent(), - required String createdAtUtc, - required String updatedAtUtc, - Value lastSuccessfulSyncAtUtc = const Value.absent(), - Value lastFullSyncAtUtc = const Value.absent(), + required String davObjectId, + required String componentType, + required String uid, + Value recurrenceIdKey = const Value.absent(), + Value sequence = const Value.absent(), + Value dtstampUtc = const Value.absent(), + Value lastModifiedUtc = const Value.absent(), + required String semanticHash, + Value parserProfileVersion = const Value.absent(), Value rowid = const Value.absent(), - }) => AccountsCompanion.insert( + }) => DavObjectComponentsCompanion.insert( id: id, - provider: provider, - providerAccountId: providerAccountId, - displayName: displayName, - email: email, - tenantId: tenantId, - accountAvatarUrl: accountAvatarUrl, - providerMetadataJson: providerMetadataJson, - authState: authState, - calendarsEnabled: calendarsEnabled, - tasksEnabled: tasksEnabled, - grantedScopes: grantedScopes, - createdAtUtc: createdAtUtc, - updatedAtUtc: updatedAtUtc, - lastSuccessfulSyncAtUtc: lastSuccessfulSyncAtUtc, - lastFullSyncAtUtc: lastFullSyncAtUtc, + davObjectId: davObjectId, + componentType: componentType, + uid: uid, + recurrenceIdKey: recurrenceIdKey, + sequence: sequence, + dtstampUtc: dtstampUtc, + lastModifiedUtc: lastModifiedUtc, + semanticHash: semanticHash, + parserProfileVersion: parserProfileVersion, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map( (e) => ( e.readTable(table), - $$AccountsTableReferences(db, table, e), + $$DavObjectComponentsTableReferences(db, table, e), ), ) .toList(), prefetchHooksCallback: ({ - taskListsRefs = false, + davObjectId = false, tasksRefs = false, - pendingOpsRefs = false, - syncRunsRefs = false, - calendarSourcesRefs = false, calendarEventsRefs = false, - calendarSyncStatesRefs = false, - scheduleItemOverridesRefs = false, - notificationScheduleRefs = false, }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (taskListsRefs) db.taskLists, if (tasksRefs) db.tasks, - if (pendingOpsRefs) db.pendingOps, - if (syncRunsRefs) db.syncRuns, - if (calendarSourcesRefs) db.calendarSources, if (calendarEventsRefs) db.calendarEvents, - if (calendarSyncStatesRefs) db.calendarSyncStates, - if (scheduleItemOverridesRefs) db.scheduleItemOverrides, - if (notificationScheduleRefs) db.notificationSchedule, ], - addJoins: null, - getPrefetchedDataCallback: (items) async { - return [ - if (taskListsRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - TaskList - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._taskListsRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).taskListsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (tasksRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - Task - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._tasksRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).tasksRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (pendingOpsRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - PendingOp - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._pendingOpsRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).pendingOpsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (syncRunsRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - SyncRun - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._syncRunsRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).syncRunsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (calendarSourcesRefs) + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (davObjectId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davObjectId, + referencedTable: + $$DavObjectComponentsTableReferences + ._davObjectIdTable(db), + referencedColumn: + $$DavObjectComponentsTableReferences + ._davObjectIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (tasksRefs) await $_getPrefetchedData< - Account, - $AccountsTable, - CalendarSource + DavObjectComponent, + $DavObjectComponentsTable, + Task >( currentTable: table, - referencedTable: $$AccountsTableReferences - ._calendarSourcesRefsTable(db), + referencedTable: $$DavObjectComponentsTableReferences + ._tasksRefsTable(db), managerFromTypedResult: (p0) => - $$AccountsTableReferences( + $$DavObjectComponentsTableReferences( db, table, p0, - ).calendarSourcesRefs, + ).tasksRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, + (e) => e.davComponentId == item.id, ), typedResults: items, ), if (calendarEventsRefs) await $_getPrefetchedData< - Account, - $AccountsTable, + DavObjectComponent, + $DavObjectComponentsTable, CalendarEvent >( currentTable: table, - referencedTable: $$AccountsTableReferences + referencedTable: $$DavObjectComponentsTableReferences ._calendarEventsRefsTable(db), managerFromTypedResult: (p0) => - $$AccountsTableReferences( + $$DavObjectComponentsTableReferences( db, table, p0, ).calendarEventsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (calendarSyncStatesRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - CalendarSyncState - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._calendarSyncStatesRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).calendarSyncStatesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, - ), - typedResults: items, - ), - if (scheduleItemOverridesRefs) - await $_getPrefetchedData< - Account, - $AccountsTable, - ScheduleItemOverride - >( - currentTable: table, - referencedTable: $$AccountsTableReferences - ._scheduleItemOverridesRefsTable(db), - managerFromTypedResult: (p0) => - $$AccountsTableReferences( - db, - table, - p0, - ).scheduleItemOverridesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, + (e) => e.davComponentId == item.id, ), typedResults: items, ), - if (notificationScheduleRefs) + ]; + }, + ); + }, + ), + ); +} + +typedef $$DavObjectComponentsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $DavObjectComponentsTable, + DavObjectComponent, + $$DavObjectComponentsTableFilterComposer, + $$DavObjectComponentsTableOrderingComposer, + $$DavObjectComponentsTableAnnotationComposer, + $$DavObjectComponentsTableCreateCompanionBuilder, + $$DavObjectComponentsTableUpdateCompanionBuilder, + (DavObjectComponent, $$DavObjectComponentsTableReferences), + DavObjectComponent, + PrefetchHooks Function({ + bool davObjectId, + bool tasksRefs, + bool calendarEventsRefs, + }) + >; +typedef $$DavConflictSnapshotsTableCreateCompanionBuilder = + DavConflictSnapshotsCompanion Function({ + required String id, + required String accountId, + Value davCollectionId, + Value davObjectId, + Value baselineEtag, + required String baselineRawIcs, + required String localCandidateRawIcs, + Value remoteEtag, + required String remoteRawIcs, + required String conflictCode, + required String createdAtUtc, + Value resolvedAtUtc, + Value resolution, + Value rowid, + }); +typedef $$DavConflictSnapshotsTableUpdateCompanionBuilder = + DavConflictSnapshotsCompanion Function({ + Value id, + Value accountId, + Value davCollectionId, + Value davObjectId, + Value baselineEtag, + Value baselineRawIcs, + Value localCandidateRawIcs, + Value remoteEtag, + Value remoteRawIcs, + Value conflictCode, + Value createdAtUtc, + Value resolvedAtUtc, + Value resolution, + Value rowid, + }); + +final class $$DavConflictSnapshotsTableReferences + extends + BaseReferences< + _$AppDatabase, + $DavConflictSnapshotsTable, + DavConflictSnapshot + > { + $$DavConflictSnapshotsTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $AccountsTable _accountIdTable(_$AppDatabase db) => + db.accounts.createAlias( + $_aliasNameGenerator(db.davConflictSnapshots.accountId, db.accounts.id), + ); + + $$AccountsTableProcessedTableManager get accountId { + final $_column = $_itemColumn('account_id')!; + + final manager = $$AccountsTableTableManager( + $_db, + $_db.accounts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.davConflictSnapshots.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectsTable _davObjectIdTable(_$AppDatabase db) => + db.davObjects.createAlias( + $_aliasNameGenerator( + db.davConflictSnapshots.davObjectId, + db.davObjects.id, + ), + ); + + $$DavObjectsTableProcessedTableManager? get davObjectId { + final $_column = $_itemColumn('dav_object_id'); + if ($_column == null) return null; + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davObjectIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static MultiTypedResultKey<$PendingOpsTable, List> + _pendingOpsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.pendingOps, + aliasName: $_aliasNameGenerator( + db.davConflictSnapshots.id, + db.pendingOps.conflictSnapshotId, + ), + ); + + $$PendingOpsTableProcessedTableManager get pendingOpsRefs { + final manager = $$PendingOpsTableTableManager($_db, $_db.pendingOps).filter( + (f) => f.conflictSnapshotId.id.sqlEquals($_itemColumn('id')!), + ); + + final cache = $_typedResult.readTableOrNull(_pendingOpsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$DavConflictSnapshotsTableFilterComposer + extends Composer<_$AppDatabase, $DavConflictSnapshotsTable> { + $$DavConflictSnapshotsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get localCandidateRawIcs => $composableBuilder( + column: $table.localCandidateRawIcs, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get remoteEtag => $composableBuilder( + column: $table.remoteEtag, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get remoteRawIcs => $composableBuilder( + column: $table.remoteRawIcs, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get conflictCode => $composableBuilder( + column: $table.conflictCode, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resolvedAtUtc => $composableBuilder( + column: $table.resolvedAtUtc, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => ColumnFilters(column), + ); + + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableFilterComposer get davObjectId { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableFilterComposer f) f, + ) { + final $$PendingOpsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.conflictSnapshotId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableFilterComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavConflictSnapshotsTableOrderingComposer + extends Composer<_$AppDatabase, $DavConflictSnapshotsTable> { + $$DavConflictSnapshotsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get localCandidateRawIcs => $composableBuilder( + column: $table.localCandidateRawIcs, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get remoteEtag => $composableBuilder( + column: $table.remoteEtag, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get remoteRawIcs => $composableBuilder( + column: $table.remoteRawIcs, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get conflictCode => $composableBuilder( + column: $table.conflictCode, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resolvedAtUtc => $composableBuilder( + column: $table.resolvedAtUtc, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => ColumnOrderings(column), + ); + + $$AccountsTableOrderingComposer get accountId { + final $$AccountsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableOrderingComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableOrderingComposer get davObjectId { + final $$DavObjectsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableOrderingComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DavConflictSnapshotsTableAnnotationComposer + extends Composer<_$AppDatabase, $DavConflictSnapshotsTable> { + $$DavConflictSnapshotsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => column, + ); + + GeneratedColumn get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => column, + ); + + GeneratedColumn get localCandidateRawIcs => $composableBuilder( + column: $table.localCandidateRawIcs, + builder: (column) => column, + ); + + GeneratedColumn get remoteEtag => $composableBuilder( + column: $table.remoteEtag, + builder: (column) => column, + ); + + GeneratedColumn get remoteRawIcs => $composableBuilder( + column: $table.remoteRawIcs, + builder: (column) => column, + ); + + GeneratedColumn get conflictCode => $composableBuilder( + column: $table.conflictCode, + builder: (column) => column, + ); + + GeneratedColumn get createdAtUtc => $composableBuilder( + column: $table.createdAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get resolvedAtUtc => $composableBuilder( + column: $table.resolvedAtUtc, + builder: (column) => column, + ); + + GeneratedColumn get resolution => $composableBuilder( + column: $table.resolution, + builder: (column) => column, + ); + + $$AccountsTableAnnotationComposer get accountId { + final $$AccountsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableAnnotationComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableAnnotationComposer get davObjectId { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + Expression pendingOpsRefs( + Expression Function($$PendingOpsTableAnnotationComposer a) f, + ) { + final $$PendingOpsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pendingOps, + getReferencedColumn: (t) => t.conflictSnapshotId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PendingOpsTableAnnotationComposer( + $db: $db, + $table: $db.pendingOps, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$DavConflictSnapshotsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $DavConflictSnapshotsTable, + DavConflictSnapshot, + $$DavConflictSnapshotsTableFilterComposer, + $$DavConflictSnapshotsTableOrderingComposer, + $$DavConflictSnapshotsTableAnnotationComposer, + $$DavConflictSnapshotsTableCreateCompanionBuilder, + $$DavConflictSnapshotsTableUpdateCompanionBuilder, + (DavConflictSnapshot, $$DavConflictSnapshotsTableReferences), + DavConflictSnapshot, + PrefetchHooks Function({ + bool accountId, + bool davCollectionId, + bool davObjectId, + bool pendingOpsRefs, + }) + > { + $$DavConflictSnapshotsTableTableManager( + _$AppDatabase db, + $DavConflictSnapshotsTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DavConflictSnapshotsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DavConflictSnapshotsTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$DavConflictSnapshotsTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value accountId = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value baselineEtag = const Value.absent(), + Value baselineRawIcs = const Value.absent(), + Value localCandidateRawIcs = const Value.absent(), + Value remoteEtag = const Value.absent(), + Value remoteRawIcs = const Value.absent(), + Value conflictCode = const Value.absent(), + Value createdAtUtc = const Value.absent(), + Value resolvedAtUtc = const Value.absent(), + Value resolution = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavConflictSnapshotsCompanion( + id: id, + accountId: accountId, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + baselineEtag: baselineEtag, + baselineRawIcs: baselineRawIcs, + localCandidateRawIcs: localCandidateRawIcs, + remoteEtag: remoteEtag, + remoteRawIcs: remoteRawIcs, + conflictCode: conflictCode, + createdAtUtc: createdAtUtc, + resolvedAtUtc: resolvedAtUtc, + resolution: resolution, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String accountId, + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value baselineEtag = const Value.absent(), + required String baselineRawIcs, + required String localCandidateRawIcs, + Value remoteEtag = const Value.absent(), + required String remoteRawIcs, + required String conflictCode, + required String createdAtUtc, + Value resolvedAtUtc = const Value.absent(), + Value resolution = const Value.absent(), + Value rowid = const Value.absent(), + }) => DavConflictSnapshotsCompanion.insert( + id: id, + accountId: accountId, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + baselineEtag: baselineEtag, + baselineRawIcs: baselineRawIcs, + localCandidateRawIcs: localCandidateRawIcs, + remoteEtag: remoteEtag, + remoteRawIcs: remoteRawIcs, + conflictCode: conflictCode, + createdAtUtc: createdAtUtc, + resolvedAtUtc: resolvedAtUtc, + resolution: resolution, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$DavConflictSnapshotsTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + accountId = false, + davCollectionId = false, + davObjectId = false, + pendingOpsRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (pendingOpsRefs) db.pendingOps], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: + $$DavConflictSnapshotsTableReferences + ._accountIdTable(db), + referencedColumn: + $$DavConflictSnapshotsTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: + $$DavConflictSnapshotsTableReferences + ._davCollectionIdTable(db), + referencedColumn: + $$DavConflictSnapshotsTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } + if (davObjectId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davObjectId, + referencedTable: + $$DavConflictSnapshotsTableReferences + ._davObjectIdTable(db), + referencedColumn: + $$DavConflictSnapshotsTableReferences + ._davObjectIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return [ + if (pendingOpsRefs) await $_getPrefetchedData< - Account, - $AccountsTable, - NotificationScheduleData + DavConflictSnapshot, + $DavConflictSnapshotsTable, + PendingOp >( currentTable: table, - referencedTable: $$AccountsTableReferences - ._notificationScheduleRefsTable(db), + referencedTable: $$DavConflictSnapshotsTableReferences + ._pendingOpsRefsTable(db), managerFromTypedResult: (p0) => - $$AccountsTableReferences( + $$DavConflictSnapshotsTableReferences( db, table, p0, - ).notificationScheduleRefs, + ).pendingOpsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where( - (e) => e.accountId == item.id, + (e) => e.conflictSnapshotId == item.id, ), typedResults: items, ), @@ -15414,34 +29520,30 @@ class $$AccountsTableTableManager ); } -typedef $$AccountsTableProcessedTableManager = +typedef $$DavConflictSnapshotsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $AccountsTable, - Account, - $$AccountsTableFilterComposer, - $$AccountsTableOrderingComposer, - $$AccountsTableAnnotationComposer, - $$AccountsTableCreateCompanionBuilder, - $$AccountsTableUpdateCompanionBuilder, - (Account, $$AccountsTableReferences), - Account, + $DavConflictSnapshotsTable, + DavConflictSnapshot, + $$DavConflictSnapshotsTableFilterComposer, + $$DavConflictSnapshotsTableOrderingComposer, + $$DavConflictSnapshotsTableAnnotationComposer, + $$DavConflictSnapshotsTableCreateCompanionBuilder, + $$DavConflictSnapshotsTableUpdateCompanionBuilder, + (DavConflictSnapshot, $$DavConflictSnapshotsTableReferences), + DavConflictSnapshot, PrefetchHooks Function({ - bool taskListsRefs, - bool tasksRefs, + bool accountId, + bool davCollectionId, + bool davObjectId, bool pendingOpsRefs, - bool syncRunsRefs, - bool calendarSourcesRefs, - bool calendarEventsRefs, - bool calendarSyncStatesRefs, - bool scheduleItemOverridesRefs, - bool notificationScheduleRefs, }) >; typedef $$TaskListsTableCreateCompanionBuilder = TaskListsCompanion Function({ required String accountId, required String id, + Value davCollectionId, Value kind, Value etag, required String title, @@ -15465,6 +29567,7 @@ typedef $$TaskListsTableUpdateCompanionBuilder = TaskListsCompanion Function({ Value accountId, Value id, + Value davCollectionId, Value kind, Value etag, Value title, @@ -15507,6 +29610,28 @@ final class $$TaskListsTableReferences manager.$state.copyWith(prefetchedData: [item]), ); } + + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.taskLists.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } } class $$TaskListsTableFilterComposer @@ -15630,6 +29755,29 @@ class $$TaskListsTableFilterComposer ); return composer; } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TaskListsTableOrderingComposer @@ -15753,6 +29901,29 @@ class $$TaskListsTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TaskListsTableAnnotationComposer @@ -15858,6 +30029,29 @@ class $$TaskListsTableAnnotationComposer ); return composer; } + + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TaskListsTableTableManager @@ -15873,7 +30067,7 @@ class $$TaskListsTableTableManager $$TaskListsTableUpdateCompanionBuilder, (TaskList, $$TaskListsTableReferences), TaskList, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({bool accountId, bool davCollectionId}) > { $$TaskListsTableTableManager(_$AppDatabase db, $TaskListsTable table) : super( @@ -15890,6 +30084,7 @@ class $$TaskListsTableTableManager ({ Value accountId = const Value.absent(), Value id = const Value.absent(), + Value davCollectionId = const Value.absent(), Value kind = const Value.absent(), Value etag = const Value.absent(), Value title = const Value.absent(), @@ -15911,6 +30106,7 @@ class $$TaskListsTableTableManager }) => TaskListsCompanion( accountId: accountId, id: id, + davCollectionId: davCollectionId, kind: kind, etag: etag, title: title, @@ -15934,6 +30130,7 @@ class $$TaskListsTableTableManager ({ required String accountId, required String id, + Value davCollectionId = const Value.absent(), Value kind = const Value.absent(), Value etag = const Value.absent(), required String title, @@ -15955,6 +30152,7 @@ class $$TaskListsTableTableManager }) => TaskListsCompanion.insert( accountId: accountId, id: id, + davCollectionId: davCollectionId, kind: kind, etag: etag, title: title, @@ -15982,47 +30180,61 @@ class $$TaskListsTableTableManager ), ) .toList(), - prefetchHooksCallback: ({accountId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (accountId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.accountId, - referencedTable: $$TaskListsTableReferences - ._accountIdTable(db), - referencedColumn: $$TaskListsTableReferences - ._accountIdTable(db) - .id, - ) - as T; - } + prefetchHooksCallback: + ({accountId = false, davCollectionId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: $$TaskListsTableReferences + ._accountIdTable(db), + referencedColumn: $$TaskListsTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: $$TaskListsTableReferences + ._davCollectionIdTable(db), + referencedColumn: $$TaskListsTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } - return state; + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; }, - getPrefetchedDataCallback: (items) async { - return []; + ); }, - ); - }, ), ); } @@ -16039,13 +30251,31 @@ typedef $$TaskListsTableProcessedTableManager = $$TaskListsTableUpdateCompanionBuilder, (TaskList, $$TaskListsTableReferences), TaskList, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({bool accountId, bool davCollectionId}) >; typedef $$TasksTableCreateCompanionBuilder = TasksCompanion Function({ required String accountId, required String taskListId, required String id, + Value davCollectionId, + Value davObjectId, + Value davComponentId, + Value icalUid, + Value recurrenceIdKey, + Value icalPriority, + Value percentComplete, + Value taskLocation, + Value taskUrl, + Value taskClassification, + Value taskPinned, + Value taskHideSubtasks, + Value taskHideCompletedSubtasks, + Value taskAlarmsJson, + Value parentUid, + Value sortOrder, + Value providerExtensionProjectionJson, + Value projectionVersion, Value kind, Value etag, required String title, @@ -16069,6 +30299,7 @@ typedef $$TasksTableCreateCompanionBuilder = Value microsoftIsReminderOn, Value microsoftCompletedDateTime, Value microsoftCompletedTimeZone, + Value microsoftChecklistItemsJson, Value recurrenceJson, Value importance, Value categoriesJson, @@ -16096,6 +30327,24 @@ typedef $$TasksTableUpdateCompanionBuilder = Value accountId, Value taskListId, Value id, + Value davCollectionId, + Value davObjectId, + Value davComponentId, + Value icalUid, + Value recurrenceIdKey, + Value icalPriority, + Value percentComplete, + Value taskLocation, + Value taskUrl, + Value taskClassification, + Value taskPinned, + Value taskHideSubtasks, + Value taskHideCompletedSubtasks, + Value taskAlarmsJson, + Value parentUid, + Value sortOrder, + Value providerExtensionProjectionJson, + Value projectionVersion, Value kind, Value etag, Value title, @@ -16119,6 +30368,7 @@ typedef $$TasksTableUpdateCompanionBuilder = Value microsoftIsReminderOn, Value microsoftCompletedDateTime, Value microsoftCompletedTimeZone, + Value microsoftChecklistItemsJson, Value recurrenceJson, Value importance, Value categoriesJson, @@ -16162,6 +30412,66 @@ final class $$TasksTableReferences manager.$state.copyWith(prefetchedData: [item]), ); } + + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator(db.tasks.davCollectionId, db.davCollections.id), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectsTable _davObjectIdTable(_$AppDatabase db) => + db.davObjects.createAlias( + $_aliasNameGenerator(db.tasks.davObjectId, db.davObjects.id), + ); + + $$DavObjectsTableProcessedTableManager? get davObjectId { + final $_column = $_itemColumn('dav_object_id'); + if ($_column == null) return null; + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davObjectIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectComponentsTable _davComponentIdTable(_$AppDatabase db) => + db.davObjectComponents.createAlias( + $_aliasNameGenerator( + db.tasks.davComponentId, + db.davObjectComponents.id, + ), + ); + + $$DavObjectComponentsTableProcessedTableManager? get davComponentId { + final $_column = $_itemColumn('dav_component_id'); + if ($_column == null) return null; + final manager = $$DavObjectComponentsTableTableManager( + $_db, + $_db.davObjectComponents, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davComponentIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } } class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> { @@ -16182,6 +30492,82 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> { builder: (column) => ColumnFilters(column), ); + ColumnFilters get icalUid => $composableBuilder( + column: $table.icalUid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get icalPriority => $composableBuilder( + column: $table.icalPriority, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get percentComplete => $composableBuilder( + column: $table.percentComplete, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskLocation => $composableBuilder( + column: $table.taskLocation, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskUrl => $composableBuilder( + column: $table.taskUrl, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskClassification => $composableBuilder( + column: $table.taskClassification, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskPinned => $composableBuilder( + column: $table.taskPinned, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskHideSubtasks => $composableBuilder( + column: $table.taskHideSubtasks, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskHideCompletedSubtasks => $composableBuilder( + column: $table.taskHideCompletedSubtasks, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get taskAlarmsJson => $composableBuilder( + column: $table.taskAlarmsJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get parentUid => $composableBuilder( + column: $table.parentUid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get providerExtensionProjectionJson => + $composableBuilder( + column: $table.providerExtensionProjectionJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get kind => $composableBuilder( column: $table.kind, builder: (column) => ColumnFilters(column), @@ -16297,6 +30683,11 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> { builder: (column) => ColumnFilters(column), ); + ColumnFilters get microsoftChecklistItemsJson => $composableBuilder( + column: $table.microsoftChecklistItemsJson, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get recurrenceJson => $composableBuilder( column: $table.recurrenceJson, builder: (column) => ColumnFilters(column), @@ -16419,6 +30810,75 @@ class $$TasksTableFilterComposer extends Composer<_$AppDatabase, $TasksTable> { ); return composer; } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableFilterComposer get davObjectId { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableFilterComposer get davComponentId { + final $$DavObjectComponentsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableFilterComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TasksTableOrderingComposer @@ -16440,6 +30900,82 @@ class $$TasksTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get icalUid => $composableBuilder( + column: $table.icalUid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get icalPriority => $composableBuilder( + column: $table.icalPriority, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get percentComplete => $composableBuilder( + column: $table.percentComplete, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskLocation => $composableBuilder( + column: $table.taskLocation, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskUrl => $composableBuilder( + column: $table.taskUrl, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskClassification => $composableBuilder( + column: $table.taskClassification, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskPinned => $composableBuilder( + column: $table.taskPinned, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskHideSubtasks => $composableBuilder( + column: $table.taskHideSubtasks, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskHideCompletedSubtasks => $composableBuilder( + column: $table.taskHideCompletedSubtasks, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get taskAlarmsJson => $composableBuilder( + column: $table.taskAlarmsJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get parentUid => $composableBuilder( + column: $table.parentUid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get sortOrder => $composableBuilder( + column: $table.sortOrder, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get providerExtensionProjectionJson => + $composableBuilder( + column: $table.providerExtensionProjectionJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get kind => $composableBuilder( column: $table.kind, builder: (column) => ColumnOrderings(column), @@ -16555,6 +31091,11 @@ class $$TasksTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get microsoftChecklistItemsJson => $composableBuilder( + column: $table.microsoftChecklistItemsJson, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get recurrenceJson => $composableBuilder( column: $table.recurrenceJson, builder: (column) => ColumnOrderings(column), @@ -16677,6 +31218,76 @@ class $$TasksTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableOrderingComposer get davObjectId { + final $$DavObjectsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableOrderingComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableOrderingComposer get davComponentId { + final $$DavObjectComponentsTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableOrderingComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TasksTableAnnotationComposer @@ -16696,6 +31307,74 @@ class $$TasksTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get icalUid => + $composableBuilder(column: $table.icalUid, builder: (column) => column); + + GeneratedColumn get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => column, + ); + + GeneratedColumn get icalPriority => $composableBuilder( + column: $table.icalPriority, + builder: (column) => column, + ); + + GeneratedColumn get percentComplete => $composableBuilder( + column: $table.percentComplete, + builder: (column) => column, + ); + + GeneratedColumn get taskLocation => $composableBuilder( + column: $table.taskLocation, + builder: (column) => column, + ); + + GeneratedColumn get taskUrl => + $composableBuilder(column: $table.taskUrl, builder: (column) => column); + + GeneratedColumn get taskClassification => $composableBuilder( + column: $table.taskClassification, + builder: (column) => column, + ); + + GeneratedColumn get taskPinned => $composableBuilder( + column: $table.taskPinned, + builder: (column) => column, + ); + + GeneratedColumn get taskHideSubtasks => $composableBuilder( + column: $table.taskHideSubtasks, + builder: (column) => column, + ); + + GeneratedColumn get taskHideCompletedSubtasks => $composableBuilder( + column: $table.taskHideCompletedSubtasks, + builder: (column) => column, + ); + + GeneratedColumn get taskAlarmsJson => $composableBuilder( + column: $table.taskAlarmsJson, + builder: (column) => column, + ); + + GeneratedColumn get parentUid => + $composableBuilder(column: $table.parentUid, builder: (column) => column); + + GeneratedColumn get sortOrder => + $composableBuilder(column: $table.sortOrder, builder: (column) => column); + + GeneratedColumn get providerExtensionProjectionJson => + $composableBuilder( + column: $table.providerExtensionProjectionJson, + builder: (column) => column, + ); + + GeneratedColumn get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => column, + ); + GeneratedColumn get kind => $composableBuilder(column: $table.kind, builder: (column) => column); @@ -16793,6 +31472,11 @@ class $$TasksTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get microsoftChecklistItemsJson => $composableBuilder( + column: $table.microsoftChecklistItemsJson, + builder: (column) => column, + ); + GeneratedColumn get recurrenceJson => $composableBuilder( column: $table.recurrenceJson, builder: (column) => column, @@ -16907,6 +31591,76 @@ class $$TasksTableAnnotationComposer ); return composer; } + + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableAnnotationComposer get davObjectId { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableAnnotationComposer get davComponentId { + final $$DavObjectComponentsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableAnnotationComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$TasksTableTableManager @@ -16922,7 +31676,12 @@ class $$TasksTableTableManager $$TasksTableUpdateCompanionBuilder, (Task, $$TasksTableReferences), Task, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({ + bool accountId, + bool davCollectionId, + bool davObjectId, + bool davComponentId, + }) > { $$TasksTableTableManager(_$AppDatabase db, $TasksTable table) : super( @@ -16940,6 +31699,25 @@ class $$TasksTableTableManager Value accountId = const Value.absent(), Value taskListId = const Value.absent(), Value id = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value icalPriority = const Value.absent(), + Value percentComplete = const Value.absent(), + Value taskLocation = const Value.absent(), + Value taskUrl = const Value.absent(), + Value taskClassification = const Value.absent(), + Value taskPinned = const Value.absent(), + Value taskHideSubtasks = const Value.absent(), + Value taskHideCompletedSubtasks = const Value.absent(), + Value taskAlarmsJson = const Value.absent(), + Value parentUid = const Value.absent(), + Value sortOrder = const Value.absent(), + Value providerExtensionProjectionJson = + const Value.absent(), + Value projectionVersion = const Value.absent(), Value kind = const Value.absent(), Value etag = const Value.absent(), Value title = const Value.absent(), @@ -16965,6 +31743,8 @@ class $$TasksTableTableManager const Value.absent(), Value microsoftCompletedTimeZone = const Value.absent(), + Value microsoftChecklistItemsJson = + const Value.absent(), Value recurrenceJson = const Value.absent(), Value importance = const Value.absent(), Value categoriesJson = const Value.absent(), @@ -16990,6 +31770,25 @@ class $$TasksTableTableManager accountId: accountId, taskListId: taskListId, id: id, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + davComponentId: davComponentId, + icalUid: icalUid, + recurrenceIdKey: recurrenceIdKey, + icalPriority: icalPriority, + percentComplete: percentComplete, + taskLocation: taskLocation, + taskUrl: taskUrl, + taskClassification: taskClassification, + taskPinned: taskPinned, + taskHideSubtasks: taskHideSubtasks, + taskHideCompletedSubtasks: taskHideCompletedSubtasks, + taskAlarmsJson: taskAlarmsJson, + parentUid: parentUid, + sortOrder: sortOrder, + providerExtensionProjectionJson: + providerExtensionProjectionJson, + projectionVersion: projectionVersion, kind: kind, etag: etag, title: title, @@ -17013,6 +31812,7 @@ class $$TasksTableTableManager microsoftIsReminderOn: microsoftIsReminderOn, microsoftCompletedDateTime: microsoftCompletedDateTime, microsoftCompletedTimeZone: microsoftCompletedTimeZone, + microsoftChecklistItemsJson: microsoftChecklistItemsJson, recurrenceJson: recurrenceJson, importance: importance, categoriesJson: categoriesJson, @@ -17040,6 +31840,25 @@ class $$TasksTableTableManager required String accountId, required String taskListId, required String id, + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value icalPriority = const Value.absent(), + Value percentComplete = const Value.absent(), + Value taskLocation = const Value.absent(), + Value taskUrl = const Value.absent(), + Value taskClassification = const Value.absent(), + Value taskPinned = const Value.absent(), + Value taskHideSubtasks = const Value.absent(), + Value taskHideCompletedSubtasks = const Value.absent(), + Value taskAlarmsJson = const Value.absent(), + Value parentUid = const Value.absent(), + Value sortOrder = const Value.absent(), + Value providerExtensionProjectionJson = + const Value.absent(), + Value projectionVersion = const Value.absent(), Value kind = const Value.absent(), Value etag = const Value.absent(), required String title, @@ -17065,6 +31884,8 @@ class $$TasksTableTableManager const Value.absent(), Value microsoftCompletedTimeZone = const Value.absent(), + Value microsoftChecklistItemsJson = + const Value.absent(), Value recurrenceJson = const Value.absent(), Value importance = const Value.absent(), Value categoriesJson = const Value.absent(), @@ -17090,6 +31911,25 @@ class $$TasksTableTableManager accountId: accountId, taskListId: taskListId, id: id, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + davComponentId: davComponentId, + icalUid: icalUid, + recurrenceIdKey: recurrenceIdKey, + icalPriority: icalPriority, + percentComplete: percentComplete, + taskLocation: taskLocation, + taskUrl: taskUrl, + taskClassification: taskClassification, + taskPinned: taskPinned, + taskHideSubtasks: taskHideSubtasks, + taskHideCompletedSubtasks: taskHideCompletedSubtasks, + taskAlarmsJson: taskAlarmsJson, + parentUid: parentUid, + sortOrder: sortOrder, + providerExtensionProjectionJson: + providerExtensionProjectionJson, + projectionVersion: projectionVersion, kind: kind, etag: etag, title: title, @@ -17113,6 +31953,7 @@ class $$TasksTableTableManager microsoftIsReminderOn: microsoftIsReminderOn, microsoftCompletedDateTime: microsoftCompletedDateTime, microsoftCompletedTimeZone: microsoftCompletedTimeZone, + microsoftChecklistItemsJson: microsoftChecklistItemsJson, recurrenceJson: recurrenceJson, importance: importance, categoriesJson: categoriesJson, @@ -17141,47 +31982,92 @@ class $$TasksTableTableManager (e.readTable(table), $$TasksTableReferences(db, table, e)), ) .toList(), - prefetchHooksCallback: ({accountId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (accountId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.accountId, - referencedTable: $$TasksTableReferences - ._accountIdTable(db), - referencedColumn: $$TasksTableReferences - ._accountIdTable(db) - .id, - ) - as T; - } + prefetchHooksCallback: + ({ + accountId = false, + davCollectionId = false, + davObjectId = false, + davComponentId = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: $$TasksTableReferences + ._accountIdTable(db), + referencedColumn: $$TasksTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: $$TasksTableReferences + ._davCollectionIdTable(db), + referencedColumn: $$TasksTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } + if (davObjectId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davObjectId, + referencedTable: $$TasksTableReferences + ._davObjectIdTable(db), + referencedColumn: $$TasksTableReferences + ._davObjectIdTable(db) + .id, + ) + as T; + } + if (davComponentId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davComponentId, + referencedTable: $$TasksTableReferences + ._davComponentIdTable(db), + referencedColumn: $$TasksTableReferences + ._davComponentIdTable(db) + .id, + ) + as T; + } - return state; + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; }, - getPrefetchedDataCallback: (items) async { - return []; + ); }, - ); - }, ), ); } @@ -17198,7 +32084,12 @@ typedef $$TasksTableProcessedTableManager = $$TasksTableUpdateCompanionBuilder, (Task, $$TasksTableReferences), Task, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({ + bool accountId, + bool davCollectionId, + bool davObjectId, + bool davComponentId, + }) >; typedef $$PendingOpsTableCreateCompanionBuilder = PendingOpsCompanion Function({ @@ -17213,6 +32104,22 @@ typedef $$PendingOpsTableCreateCompanionBuilder = Value calendarSourceId, Value providerCalendarId, Value eventId, + Value davCollectionId, + Value davCollectionHref, + Value davObjectId, + Value davMemberHref, + Value baselineEtag, + Value baselineRawIcs, + Value mutationPatchJson, + Value mutationPatchSchemaVersion, + Value targetComponentKey, + Value mutationScope, + Value destinationCollectionId, + Value destinationCollectionHref, + Value destinationMemberHref, + Value conflictState, + Value conflictSnapshotId, + Value retryClassification, Value localTempId, Value dependsOnOpId, required String requestJson, @@ -17241,6 +32148,22 @@ typedef $$PendingOpsTableUpdateCompanionBuilder = Value calendarSourceId, Value providerCalendarId, Value eventId, + Value davCollectionId, + Value davCollectionHref, + Value davObjectId, + Value davMemberHref, + Value baselineEtag, + Value baselineRawIcs, + Value mutationPatchJson, + Value mutationPatchSchemaVersion, + Value targetComponentKey, + Value mutationScope, + Value destinationCollectionId, + Value destinationCollectionHref, + Value destinationMemberHref, + Value conflictState, + Value conflictSnapshotId, + Value retryClassification, Value localTempId, Value dependsOnOpId, Value requestJson, @@ -17269,11 +32192,99 @@ final class $$PendingOpsTableReferences $$AccountsTableProcessedTableManager get accountId { final $_column = $_itemColumn('account_id')!; - final manager = $$AccountsTableTableManager( + final manager = $$AccountsTableTableManager( + $_db, + $_db.accounts, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.pendingOps.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectsTable _davObjectIdTable(_$AppDatabase db) => + db.davObjects.createAlias( + $_aliasNameGenerator(db.pendingOps.davObjectId, db.davObjects.id), + ); + + $$DavObjectsTableProcessedTableManager? get davObjectId { + final $_column = $_itemColumn('dav_object_id'); + if ($_column == null) return null; + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davObjectIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavCollectionsTable _destinationCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.pendingOps.destinationCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get destinationCollectionId { + final $_column = $_itemColumn('destination_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull( + _destinationCollectionIdTable($_db), + ); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavConflictSnapshotsTable _conflictSnapshotIdTable( + _$AppDatabase db, + ) => db.davConflictSnapshots.createAlias( + $_aliasNameGenerator( + db.pendingOps.conflictSnapshotId, + db.davConflictSnapshots.id, + ), + ); + + $$DavConflictSnapshotsTableProcessedTableManager? get conflictSnapshotId { + final $_column = $_itemColumn('conflict_snapshot_id'); + if ($_column == null) return null; + final manager = $$DavConflictSnapshotsTableTableManager( $_db, - $_db.accounts, + $_db.davConflictSnapshots, ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_accountIdTable($_db)); + final item = $_typedResult.readTableOrNull(_conflictSnapshotIdTable($_db)); if (item == null) return manager; return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item]), @@ -17340,6 +32351,66 @@ class $$PendingOpsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get davCollectionHref => $composableBuilder( + column: $table.davCollectionHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get davMemberHref => $composableBuilder( + column: $table.davMemberHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mutationPatchJson => $composableBuilder( + column: $table.mutationPatchJson, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mutationPatchSchemaVersion => $composableBuilder( + column: $table.mutationPatchSchemaVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get targetComponentKey => $composableBuilder( + column: $table.targetComponentKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get mutationScope => $composableBuilder( + column: $table.mutationScope, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get destinationCollectionHref => $composableBuilder( + column: $table.destinationCollectionHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get destinationMemberHref => $composableBuilder( + column: $table.destinationMemberHref, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get conflictState => $composableBuilder( + column: $table.conflictState, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get retryClassification => $composableBuilder( + column: $table.retryClassification, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get localTempId => $composableBuilder( column: $table.localTempId, builder: (column) => ColumnFilters(column), @@ -17427,6 +32498,98 @@ class $$PendingOpsTableFilterComposer ); return composer; } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableFilterComposer get davObjectId { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableFilterComposer get destinationCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.destinationCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavConflictSnapshotsTableFilterComposer get conflictSnapshotId { + final $$DavConflictSnapshotsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conflictSnapshotId, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableFilterComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$PendingOpsTableOrderingComposer @@ -17488,6 +32651,66 @@ class $$PendingOpsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get davCollectionHref => $composableBuilder( + column: $table.davCollectionHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get davMemberHref => $composableBuilder( + column: $table.davMemberHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mutationPatchJson => $composableBuilder( + column: $table.mutationPatchJson, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mutationPatchSchemaVersion => $composableBuilder( + column: $table.mutationPatchSchemaVersion, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get targetComponentKey => $composableBuilder( + column: $table.targetComponentKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get mutationScope => $composableBuilder( + column: $table.mutationScope, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get destinationCollectionHref => $composableBuilder( + column: $table.destinationCollectionHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get destinationMemberHref => $composableBuilder( + column: $table.destinationMemberHref, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get conflictState => $composableBuilder( + column: $table.conflictState, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get retryClassification => $composableBuilder( + column: $table.retryClassification, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get localTempId => $composableBuilder( column: $table.localTempId, builder: (column) => ColumnOrderings(column), @@ -17575,6 +32798,99 @@ class $$PendingOpsTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableOrderingComposer get davObjectId { + final $$DavObjectsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableOrderingComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableOrderingComposer get destinationCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.destinationCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavConflictSnapshotsTableOrderingComposer get conflictSnapshotId { + final $$DavConflictSnapshotsTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conflictSnapshotId, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableOrderingComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$PendingOpsTableAnnotationComposer @@ -17597,34 +32913,94 @@ class $$PendingOpsTableAnnotationComposer builder: (column) => column, ); - GeneratedColumn get operation => - $composableBuilder(column: $table.operation, builder: (column) => column); + GeneratedColumn get operation => + $composableBuilder(column: $table.operation, builder: (column) => column); + + GeneratedColumn get operationType => $composableBuilder( + column: $table.operationType, + builder: (column) => column, + ); + + GeneratedColumn get taskListId => $composableBuilder( + column: $table.taskListId, + builder: (column) => column, + ); + + GeneratedColumn get taskId => + $composableBuilder(column: $table.taskId, builder: (column) => column); + + GeneratedColumn get calendarSourceId => $composableBuilder( + column: $table.calendarSourceId, + builder: (column) => column, + ); + + GeneratedColumn get providerCalendarId => $composableBuilder( + column: $table.providerCalendarId, + builder: (column) => column, + ); + + GeneratedColumn get eventId => + $composableBuilder(column: $table.eventId, builder: (column) => column); + + GeneratedColumn get davCollectionHref => $composableBuilder( + column: $table.davCollectionHref, + builder: (column) => column, + ); + + GeneratedColumn get davMemberHref => $composableBuilder( + column: $table.davMemberHref, + builder: (column) => column, + ); + + GeneratedColumn get baselineEtag => $composableBuilder( + column: $table.baselineEtag, + builder: (column) => column, + ); + + GeneratedColumn get baselineRawIcs => $composableBuilder( + column: $table.baselineRawIcs, + builder: (column) => column, + ); + + GeneratedColumn get mutationPatchJson => $composableBuilder( + column: $table.mutationPatchJson, + builder: (column) => column, + ); + + GeneratedColumn get mutationPatchSchemaVersion => $composableBuilder( + column: $table.mutationPatchSchemaVersion, + builder: (column) => column, + ); - GeneratedColumn get operationType => $composableBuilder( - column: $table.operationType, + GeneratedColumn get targetComponentKey => $composableBuilder( + column: $table.targetComponentKey, builder: (column) => column, ); - GeneratedColumn get taskListId => $composableBuilder( - column: $table.taskListId, + GeneratedColumn get mutationScope => $composableBuilder( + column: $table.mutationScope, builder: (column) => column, ); - GeneratedColumn get taskId => - $composableBuilder(column: $table.taskId, builder: (column) => column); + GeneratedColumn get destinationCollectionHref => $composableBuilder( + column: $table.destinationCollectionHref, + builder: (column) => column, + ); - GeneratedColumn get calendarSourceId => $composableBuilder( - column: $table.calendarSourceId, + GeneratedColumn get destinationMemberHref => $composableBuilder( + column: $table.destinationMemberHref, builder: (column) => column, ); - GeneratedColumn get providerCalendarId => $composableBuilder( - column: $table.providerCalendarId, + GeneratedColumn get conflictState => $composableBuilder( + column: $table.conflictState, builder: (column) => column, ); - GeneratedColumn get eventId => - $composableBuilder(column: $table.eventId, builder: (column) => column); + GeneratedColumn get retryClassification => $composableBuilder( + column: $table.retryClassification, + builder: (column) => column, + ); GeneratedColumn get localTempId => $composableBuilder( column: $table.localTempId, @@ -17709,6 +33085,99 @@ class $$PendingOpsTableAnnotationComposer ); return composer; } + + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableAnnotationComposer get davObjectId { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableAnnotationComposer get destinationCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.destinationCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavConflictSnapshotsTableAnnotationComposer get conflictSnapshotId { + final $$DavConflictSnapshotsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.conflictSnapshotId, + referencedTable: $db.davConflictSnapshots, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavConflictSnapshotsTableAnnotationComposer( + $db: $db, + $table: $db.davConflictSnapshots, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$PendingOpsTableTableManager @@ -17724,7 +33193,13 @@ class $$PendingOpsTableTableManager $$PendingOpsTableUpdateCompanionBuilder, (PendingOp, $$PendingOpsTableReferences), PendingOp, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({ + bool accountId, + bool davCollectionId, + bool davObjectId, + bool destinationCollectionId, + bool conflictSnapshotId, + }) > { $$PendingOpsTableTableManager(_$AppDatabase db, $PendingOpsTable table) : super( @@ -17750,6 +33225,22 @@ class $$PendingOpsTableTableManager Value calendarSourceId = const Value.absent(), Value providerCalendarId = const Value.absent(), Value eventId = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davCollectionHref = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davMemberHref = const Value.absent(), + Value baselineEtag = const Value.absent(), + Value baselineRawIcs = const Value.absent(), + Value mutationPatchJson = const Value.absent(), + Value mutationPatchSchemaVersion = const Value.absent(), + Value targetComponentKey = const Value.absent(), + Value mutationScope = const Value.absent(), + Value destinationCollectionId = const Value.absent(), + Value destinationCollectionHref = const Value.absent(), + Value destinationMemberHref = const Value.absent(), + Value conflictState = const Value.absent(), + Value conflictSnapshotId = const Value.absent(), + Value retryClassification = const Value.absent(), Value localTempId = const Value.absent(), Value dependsOnOpId = const Value.absent(), Value requestJson = const Value.absent(), @@ -17776,6 +33267,22 @@ class $$PendingOpsTableTableManager calendarSourceId: calendarSourceId, providerCalendarId: providerCalendarId, eventId: eventId, + davCollectionId: davCollectionId, + davCollectionHref: davCollectionHref, + davObjectId: davObjectId, + davMemberHref: davMemberHref, + baselineEtag: baselineEtag, + baselineRawIcs: baselineRawIcs, + mutationPatchJson: mutationPatchJson, + mutationPatchSchemaVersion: mutationPatchSchemaVersion, + targetComponentKey: targetComponentKey, + mutationScope: mutationScope, + destinationCollectionId: destinationCollectionId, + destinationCollectionHref: destinationCollectionHref, + destinationMemberHref: destinationMemberHref, + conflictState: conflictState, + conflictSnapshotId: conflictSnapshotId, + retryClassification: retryClassification, localTempId: localTempId, dependsOnOpId: dependsOnOpId, requestJson: requestJson, @@ -17804,6 +33311,22 @@ class $$PendingOpsTableTableManager Value calendarSourceId = const Value.absent(), Value providerCalendarId = const Value.absent(), Value eventId = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davCollectionHref = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davMemberHref = const Value.absent(), + Value baselineEtag = const Value.absent(), + Value baselineRawIcs = const Value.absent(), + Value mutationPatchJson = const Value.absent(), + Value mutationPatchSchemaVersion = const Value.absent(), + Value targetComponentKey = const Value.absent(), + Value mutationScope = const Value.absent(), + Value destinationCollectionId = const Value.absent(), + Value destinationCollectionHref = const Value.absent(), + Value destinationMemberHref = const Value.absent(), + Value conflictState = const Value.absent(), + Value conflictSnapshotId = const Value.absent(), + Value retryClassification = const Value.absent(), Value localTempId = const Value.absent(), Value dependsOnOpId = const Value.absent(), required String requestJson, @@ -17830,6 +33353,22 @@ class $$PendingOpsTableTableManager calendarSourceId: calendarSourceId, providerCalendarId: providerCalendarId, eventId: eventId, + davCollectionId: davCollectionId, + davCollectionHref: davCollectionHref, + davObjectId: davObjectId, + davMemberHref: davMemberHref, + baselineEtag: baselineEtag, + baselineRawIcs: baselineRawIcs, + mutationPatchJson: mutationPatchJson, + mutationPatchSchemaVersion: mutationPatchSchemaVersion, + targetComponentKey: targetComponentKey, + mutationScope: mutationScope, + destinationCollectionId: destinationCollectionId, + destinationCollectionHref: destinationCollectionHref, + destinationMemberHref: destinationMemberHref, + conflictState: conflictState, + conflictSnapshotId: conflictSnapshotId, + retryClassification: retryClassification, localTempId: localTempId, dependsOnOpId: dependsOnOpId, requestJson: requestJson, @@ -17853,47 +33392,112 @@ class $$PendingOpsTableTableManager ), ) .toList(), - prefetchHooksCallback: ({accountId = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [], - addJoins: - < - T extends TableManagerState< - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic, - dynamic - > - >(state) { - if (accountId) { - state = - state.withJoin( - currentTable: table, - currentColumn: table.accountId, - referencedTable: $$PendingOpsTableReferences - ._accountIdTable(db), - referencedColumn: $$PendingOpsTableReferences - ._accountIdTable(db) - .id, - ) - as T; - } + prefetchHooksCallback: + ({ + accountId = false, + davCollectionId = false, + davObjectId = false, + destinationCollectionId = false, + conflictSnapshotId = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (accountId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.accountId, + referencedTable: $$PendingOpsTableReferences + ._accountIdTable(db), + referencedColumn: + $$PendingOpsTableReferences + ._accountIdTable(db) + .id, + ) + as T; + } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: $$PendingOpsTableReferences + ._davCollectionIdTable(db), + referencedColumn: + $$PendingOpsTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } + if (davObjectId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davObjectId, + referencedTable: $$PendingOpsTableReferences + ._davObjectIdTable(db), + referencedColumn: + $$PendingOpsTableReferences + ._davObjectIdTable(db) + .id, + ) + as T; + } + if (destinationCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: + table.destinationCollectionId, + referencedTable: $$PendingOpsTableReferences + ._destinationCollectionIdTable(db), + referencedColumn: + $$PendingOpsTableReferences + ._destinationCollectionIdTable(db) + .id, + ) + as T; + } + if (conflictSnapshotId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.conflictSnapshotId, + referencedTable: $$PendingOpsTableReferences + ._conflictSnapshotIdTable(db), + referencedColumn: + $$PendingOpsTableReferences + ._conflictSnapshotIdTable(db) + .id, + ) + as T; + } - return state; + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; }, - getPrefetchedDataCallback: (items) async { - return []; + ); }, - ); - }, ), ); } @@ -17910,7 +33514,13 @@ typedef $$PendingOpsTableProcessedTableManager = $$PendingOpsTableUpdateCompanionBuilder, (PendingOp, $$PendingOpsTableReferences), PendingOp, - PrefetchHooks Function({bool accountId}) + PrefetchHooks Function({ + bool accountId, + bool davCollectionId, + bool davObjectId, + bool destinationCollectionId, + bool conflictSnapshotId, + }) >; typedef $$SyncRunsTableCreateCompanionBuilder = SyncRunsCompanion Function({ @@ -18378,6 +33988,7 @@ typedef $$CalendarSourcesTableCreateCompanionBuilder = required String accountId, required String provider, required String providerCalendarId, + Value davCollectionId, required String summary, Value description, Value primaryCalendar, @@ -18401,6 +34012,7 @@ typedef $$CalendarSourcesTableUpdateCompanionBuilder = Value accountId, Value provider, Value providerCalendarId, + Value davCollectionId, Value summary, Value description, Value primaryCalendar, @@ -18447,6 +34059,28 @@ final class $$CalendarSourcesTableReferences ); } + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.calendarSources.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + static MultiTypedResultKey<$CalendarEventsTable, List> _calendarEventsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.calendarEvents, @@ -18468,28 +34102,22 @@ final class $$CalendarSourcesTableReferences ); } - static MultiTypedResultKey<$CalendarSyncStatesTable, List> - _calendarSyncStatesRefsTable(_$AppDatabase db) => - MultiTypedResultKey.fromTable( - db.calendarSyncStates, - aliasName: $_aliasNameGenerator( - db.calendarSources.id, - db.calendarSyncStates.calendarSourceId, - ), - ); + static MultiTypedResultKey<$SyncCursorsTable, List> + _syncCursorsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.syncCursors, + aliasName: $_aliasNameGenerator( + db.calendarSources.id, + db.syncCursors.projectionSourceId, + ), + ); - $$CalendarSyncStatesTableProcessedTableManager get calendarSyncStatesRefs { - final manager = - $$CalendarSyncStatesTableTableManager( - $_db, - $_db.calendarSyncStates, - ).filter( - (f) => f.calendarSourceId.id.sqlEquals($_itemColumn('id')!), + $$SyncCursorsTableProcessedTableManager get syncCursorsRefs { + final manager = $$SyncCursorsTableTableManager($_db, $_db.syncCursors) + .filter( + (f) => f.projectionSourceId.id.sqlEquals($_itemColumn('id')!), ); - final cache = $_typedResult.readTableOrNull( - _calendarSyncStatesRefsTable($_db), - ); + final cache = $_typedResult.readTableOrNull(_syncCursorsRefsTable($_db)); return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache), ); @@ -18595,20 +34223,43 @@ class $$CalendarSourcesTableFilterComposer builder: (column) => ColumnFilters(column), ); - $$AccountsTableFilterComposer get accountId { - final $$AccountsTableFilterComposer composer = $composerBuilder( + $$AccountsTableFilterComposer get accountId { + final $$AccountsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.accountId, + referencedTable: $db.accounts, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$AccountsTableFilterComposer( + $db: $db, + $table: $db.accounts, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.accountId, - referencedTable: $db.accounts, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => $$AccountsTableFilterComposer( + }) => $$DavCollectionsTableFilterComposer( $db: $db, - $table: $db.accounts, + $table: $db.davCollections, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -18643,22 +34294,22 @@ class $$CalendarSourcesTableFilterComposer return f(composer); } - Expression calendarSyncStatesRefs( - Expression Function($$CalendarSyncStatesTableFilterComposer f) f, + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableFilterComposer f) f, ) { - final $$CalendarSyncStatesTableFilterComposer composer = $composerBuilder( + final $$SyncCursorsTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSyncStates, - getReferencedColumn: (t) => t.calendarSourceId, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.projectionSourceId, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => $$CalendarSyncStatesTableFilterComposer( + }) => $$SyncCursorsTableFilterComposer( $db: $db, - $table: $db.calendarSyncStates, + $table: $db.syncCursors, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -18790,6 +34441,29 @@ class $$CalendarSourcesTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$CalendarSourcesTableAnnotationComposer @@ -18894,6 +34568,29 @@ class $$CalendarSourcesTableAnnotationComposer return composer; } + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + Expression calendarEventsRefs( Expression Function($$CalendarEventsTableAnnotationComposer a) f, ) { @@ -18919,29 +34616,28 @@ class $$CalendarSourcesTableAnnotationComposer return f(composer); } - Expression calendarSyncStatesRefs( - Expression Function($$CalendarSyncStatesTableAnnotationComposer a) f, + Expression syncCursorsRefs( + Expression Function($$SyncCursorsTableAnnotationComposer a) f, ) { - final $$CalendarSyncStatesTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.calendarSyncStates, - getReferencedColumn: (t) => t.calendarSourceId, - builder: - ( - joinBuilder, { - $addJoinBuilderToRootComposer, + final $$SyncCursorsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.syncCursors, + getReferencedColumn: (t) => t.projectionSourceId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$SyncCursorsTableAnnotationComposer( + $db: $db, + $table: $db.syncCursors, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, - }) => $$CalendarSyncStatesTableAnnotationComposer( - $db: $db, - $table: $db.calendarSyncStates, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - ), - ); + ), + ); return f(composer); } } @@ -18961,8 +34657,9 @@ class $$CalendarSourcesTableTableManager CalendarSource, PrefetchHooks Function({ bool accountId, + bool davCollectionId, bool calendarEventsRefs, - bool calendarSyncStatesRefs, + bool syncCursorsRefs, }) > { $$CalendarSourcesTableTableManager( @@ -18984,6 +34681,7 @@ class $$CalendarSourcesTableTableManager Value accountId = const Value.absent(), Value provider = const Value.absent(), Value providerCalendarId = const Value.absent(), + Value davCollectionId = const Value.absent(), Value summary = const Value.absent(), Value description = const Value.absent(), Value primaryCalendar = const Value.absent(), @@ -19005,6 +34703,7 @@ class $$CalendarSourcesTableTableManager accountId: accountId, provider: provider, providerCalendarId: providerCalendarId, + davCollectionId: davCollectionId, summary: summary, description: description, primaryCalendar: primaryCalendar, @@ -19028,6 +34727,7 @@ class $$CalendarSourcesTableTableManager required String accountId, required String provider, required String providerCalendarId, + Value davCollectionId = const Value.absent(), required String summary, Value description = const Value.absent(), Value primaryCalendar = const Value.absent(), @@ -19049,6 +34749,7 @@ class $$CalendarSourcesTableTableManager accountId: accountId, provider: provider, providerCalendarId: providerCalendarId, + davCollectionId: davCollectionId, summary: summary, description: description, primaryCalendar: primaryCalendar, @@ -19077,14 +34778,15 @@ class $$CalendarSourcesTableTableManager prefetchHooksCallback: ({ accountId = false, + davCollectionId = false, calendarEventsRefs = false, - calendarSyncStatesRefs = false, + syncCursorsRefs = false, }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (calendarEventsRefs) db.calendarEvents, - if (calendarSyncStatesRefs) db.calendarSyncStates, + if (syncCursorsRefs) db.syncCursors, ], addJoins: < @@ -19117,6 +34819,21 @@ class $$CalendarSourcesTableTableManager ) as T; } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: + $$CalendarSourcesTableReferences + ._davCollectionIdTable(db), + referencedColumn: + $$CalendarSourcesTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } return state; }, @@ -19143,24 +34860,24 @@ class $$CalendarSourcesTableTableManager ), typedResults: items, ), - if (calendarSyncStatesRefs) + if (syncCursorsRefs) await $_getPrefetchedData< CalendarSource, $CalendarSourcesTable, - CalendarSyncState + SyncCursor >( currentTable: table, referencedTable: $$CalendarSourcesTableReferences - ._calendarSyncStatesRefsTable(db), + ._syncCursorsRefsTable(db), managerFromTypedResult: (p0) => $$CalendarSourcesTableReferences( db, table, p0, - ).calendarSyncStatesRefs, + ).syncCursorsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems.where( - (e) => e.calendarSourceId == item.id, + (e) => e.projectionSourceId == item.id, ), typedResults: items, ), @@ -19186,8 +34903,9 @@ typedef $$CalendarSourcesTableProcessedTableManager = CalendarSource, PrefetchHooks Function({ bool accountId, + bool davCollectionId, bool calendarEventsRefs, - bool calendarSyncStatesRefs, + bool syncCursorsRefs, }) >; typedef $$CalendarEventsTableCreateCompanionBuilder = @@ -19198,6 +34916,13 @@ typedef $$CalendarEventsTableCreateCompanionBuilder = required String provider, required String providerCalendarId, required String providerEventId, + Value davCollectionId, + Value davObjectId, + Value davComponentId, + Value icalUid, + Value recurrenceIdKey, + Value occurrenceKey, + Value projectionVersion, Value providerRecurringEventId, Value providerOriginalStartKey, Value etagOrChangeKey, @@ -19245,6 +34970,13 @@ typedef $$CalendarEventsTableUpdateCompanionBuilder = Value provider, Value providerCalendarId, Value providerEventId, + Value davCollectionId, + Value davObjectId, + Value davComponentId, + Value icalUid, + Value recurrenceIdKey, + Value occurrenceKey, + Value projectionVersion, Value providerRecurringEventId, Value providerOriginalStartKey, Value etagOrChangeKey, @@ -19334,6 +35066,69 @@ final class $$CalendarEventsTableReferences ); } + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.calendarEvents.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectsTable _davObjectIdTable(_$AppDatabase db) => + db.davObjects.createAlias( + $_aliasNameGenerator(db.calendarEvents.davObjectId, db.davObjects.id), + ); + + $$DavObjectsTableProcessedTableManager? get davObjectId { + final $_column = $_itemColumn('dav_object_id'); + if ($_column == null) return null; + final manager = $$DavObjectsTableTableManager( + $_db, + $_db.davObjects, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davObjectIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavObjectComponentsTable _davComponentIdTable(_$AppDatabase db) => + db.davObjectComponents.createAlias( + $_aliasNameGenerator( + db.calendarEvents.davComponentId, + db.davObjectComponents.id, + ), + ); + + $$DavObjectComponentsTableProcessedTableManager? get davComponentId { + final $_column = $_itemColumn('dav_component_id'); + if ($_column == null) return null; + final manager = $$DavObjectComponentsTableTableManager( + $_db, + $_db.davObjectComponents, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davComponentIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + static MultiTypedResultKey< $CalendarEventAttendeesTable, List @@ -19426,6 +35221,26 @@ class $$CalendarEventsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get icalUid => $composableBuilder( + column: $table.icalUid, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get occurrenceKey => $composableBuilder( + column: $table.occurrenceKey, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get providerRecurringEventId => $composableBuilder( column: $table.providerRecurringEventId, builder: (column) => ColumnFilters(column), @@ -19657,6 +35472,75 @@ class $$CalendarEventsTableFilterComposer return composer; } + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableFilterComposer get davObjectId { + final $$DavObjectsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableFilterComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableFilterComposer get davComponentId { + final $$DavObjectComponentsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableFilterComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + Expression calendarEventAttendeesRefs( Expression Function($$CalendarEventAttendeesTableFilterComposer f) f, ) { @@ -19739,6 +35623,26 @@ class $$CalendarEventsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get icalUid => $composableBuilder( + column: $table.icalUid, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get occurrenceKey => $composableBuilder( + column: $table.occurrenceKey, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get projectionVersion => $composableBuilder( + column: $table.projectionVersion, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get providerRecurringEventId => $composableBuilder( column: $table.providerRecurringEventId, builder: (column) => ColumnOrderings(column), @@ -19969,6 +35873,76 @@ class $$CalendarEventsTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableOrderingComposer get davObjectId { + final $$DavObjectsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableOrderingComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableOrderingComposer get davComponentId { + final $$DavObjectComponentsTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableOrderingComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$CalendarEventsTableAnnotationComposer @@ -19986,13 +35960,31 @@ class $$CalendarEventsTableAnnotationComposer GeneratedColumn get provider => $composableBuilder(column: $table.provider, builder: (column) => column); - GeneratedColumn get providerCalendarId => $composableBuilder( - column: $table.providerCalendarId, + GeneratedColumn get providerCalendarId => $composableBuilder( + column: $table.providerCalendarId, + builder: (column) => column, + ); + + GeneratedColumn get providerEventId => $composableBuilder( + column: $table.providerEventId, + builder: (column) => column, + ); + + GeneratedColumn get icalUid => + $composableBuilder(column: $table.icalUid, builder: (column) => column); + + GeneratedColumn get recurrenceIdKey => $composableBuilder( + column: $table.recurrenceIdKey, + builder: (column) => column, + ); + + GeneratedColumn get occurrenceKey => $composableBuilder( + column: $table.occurrenceKey, builder: (column) => column, ); - GeneratedColumn get providerEventId => $composableBuilder( - column: $table.providerEventId, + GeneratedColumn get projectionVersion => $composableBuilder( + column: $table.projectionVersion, builder: (column) => column, ); @@ -20203,6 +36195,76 @@ class $$CalendarEventsTableAnnotationComposer return composer; } + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectsTableAnnotationComposer get davObjectId { + final $$DavObjectsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davObjectId, + referencedTable: $db.davObjects, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectsTableAnnotationComposer( + $db: $db, + $table: $db.davObjects, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$DavObjectComponentsTableAnnotationComposer get davComponentId { + final $$DavObjectComponentsTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davComponentId, + referencedTable: $db.davObjectComponents, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavObjectComponentsTableAnnotationComposer( + $db: $db, + $table: $db.davObjectComponents, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + Expression calendarEventAttendeesRefs( Expression Function($$CalendarEventAttendeesTableAnnotationComposer a) f, ) { @@ -20272,6 +36334,9 @@ class $$CalendarEventsTableTableManager PrefetchHooks Function({ bool accountId, bool calendarSourceId, + bool davCollectionId, + bool davObjectId, + bool davComponentId, bool calendarEventAttendeesRefs, bool calendarEventRemindersRefs, }) @@ -20297,6 +36362,13 @@ class $$CalendarEventsTableTableManager Value provider = const Value.absent(), Value providerCalendarId = const Value.absent(), Value providerEventId = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value occurrenceKey = const Value.absent(), + Value projectionVersion = const Value.absent(), Value providerRecurringEventId = const Value.absent(), Value providerOriginalStartKey = const Value.absent(), Value etagOrChangeKey = const Value.absent(), @@ -20342,6 +36414,13 @@ class $$CalendarEventsTableTableManager provider: provider, providerCalendarId: providerCalendarId, providerEventId: providerEventId, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + davComponentId: davComponentId, + icalUid: icalUid, + recurrenceIdKey: recurrenceIdKey, + occurrenceKey: occurrenceKey, + projectionVersion: projectionVersion, providerRecurringEventId: providerRecurringEventId, providerOriginalStartKey: providerOriginalStartKey, etagOrChangeKey: etagOrChangeKey, @@ -20389,6 +36468,13 @@ class $$CalendarEventsTableTableManager required String provider, required String providerCalendarId, required String providerEventId, + Value davCollectionId = const Value.absent(), + Value davObjectId = const Value.absent(), + Value davComponentId = const Value.absent(), + Value icalUid = const Value.absent(), + Value recurrenceIdKey = const Value.absent(), + Value occurrenceKey = const Value.absent(), + Value projectionVersion = const Value.absent(), Value providerRecurringEventId = const Value.absent(), Value providerOriginalStartKey = const Value.absent(), Value etagOrChangeKey = const Value.absent(), @@ -20434,6 +36520,13 @@ class $$CalendarEventsTableTableManager provider: provider, providerCalendarId: providerCalendarId, providerEventId: providerEventId, + davCollectionId: davCollectionId, + davObjectId: davObjectId, + davComponentId: davComponentId, + icalUid: icalUid, + recurrenceIdKey: recurrenceIdKey, + occurrenceKey: occurrenceKey, + projectionVersion: projectionVersion, providerRecurringEventId: providerRecurringEventId, providerOriginalStartKey: providerOriginalStartKey, etagOrChangeKey: etagOrChangeKey, @@ -20485,6 +36578,9 @@ class $$CalendarEventsTableTableManager ({ accountId = false, calendarSourceId = false, + davCollectionId = false, + davObjectId = false, + davComponentId = false, calendarEventAttendeesRefs = false, calendarEventRemindersRefs = false, }) { @@ -20540,6 +36636,51 @@ class $$CalendarEventsTableTableManager ) as T; } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: + $$CalendarEventsTableReferences + ._davCollectionIdTable(db), + referencedColumn: + $$CalendarEventsTableReferences + ._davCollectionIdTable(db) + .id, + ) + as T; + } + if (davObjectId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davObjectId, + referencedTable: + $$CalendarEventsTableReferences + ._davObjectIdTable(db), + referencedColumn: + $$CalendarEventsTableReferences + ._davObjectIdTable(db) + .id, + ) + as T; + } + if (davComponentId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davComponentId, + referencedTable: + $$CalendarEventsTableReferences + ._davComponentIdTable(db), + referencedColumn: + $$CalendarEventsTableReferences + ._davComponentIdTable(db) + .id, + ) + as T; + } return state; }, @@ -20610,6 +36751,9 @@ typedef $$CalendarEventsTableProcessedTableManager = PrefetchHooks Function({ bool accountId, bool calendarSourceId, + bool davCollectionId, + bool davObjectId, + bool davComponentId, bool calendarEventAttendeesRefs, bool calendarEventRemindersRefs, }) @@ -21443,57 +37587,58 @@ typedef $$CalendarEventRemindersTableProcessedTableManager = CalendarEventReminder, PrefetchHooks Function({bool calendarEventId}) >; -typedef $$CalendarSyncStatesTableCreateCompanionBuilder = - CalendarSyncStatesCompanion Function({ +typedef $$SyncCursorsTableCreateCompanionBuilder = + SyncCursorsCompanion Function({ required String id, required String accountId, - Value calendarSourceId, + Value projectionSourceId, required String provider, - required String syncKind, + required String transport, + required String syncScopeKind, + Value davCollectionId, + required String cursorKind, + required String cursorValue, Value rangeStart, Value rangeEnd, - Value googleSyncToken, - Value microsoftDeltaLink, - Value lastFullSyncAt, - Value lastIncrementalSyncAt, - Value lastError, - Value rawStateJson, + Value baselineGeneration, + Value inProgressCursor, + Value inProgressGeneration, + Value lastCompleteSyncAt, + Value lastFailureCode, + Value stateSchemaVersion, + Value stateJson, Value rowid, }); -typedef $$CalendarSyncStatesTableUpdateCompanionBuilder = - CalendarSyncStatesCompanion Function({ +typedef $$SyncCursorsTableUpdateCompanionBuilder = + SyncCursorsCompanion Function({ Value id, Value accountId, - Value calendarSourceId, + Value projectionSourceId, Value provider, - Value syncKind, + Value transport, + Value syncScopeKind, + Value davCollectionId, + Value cursorKind, + Value cursorValue, Value rangeStart, Value rangeEnd, - Value googleSyncToken, - Value microsoftDeltaLink, - Value lastFullSyncAt, - Value lastIncrementalSyncAt, - Value lastError, - Value rawStateJson, + Value baselineGeneration, + Value inProgressCursor, + Value inProgressGeneration, + Value lastCompleteSyncAt, + Value lastFailureCode, + Value stateSchemaVersion, + Value stateJson, Value rowid, }); -final class $$CalendarSyncStatesTableReferences - extends - BaseReferences< - _$AppDatabase, - $CalendarSyncStatesTable, - CalendarSyncState - > { - $$CalendarSyncStatesTableReferences( - super.$_db, - super.$_table, - super.$_typedResult, - ); +final class $$SyncCursorsTableReferences + extends BaseReferences<_$AppDatabase, $SyncCursorsTable, SyncCursor> { + $$SyncCursorsTableReferences(super.$_db, super.$_table, super.$_typedResult); static $AccountsTable _accountIdTable(_$AppDatabase db) => db.accounts.createAlias( - $_aliasNameGenerator(db.calendarSyncStates.accountId, db.accounts.id), + $_aliasNameGenerator(db.syncCursors.accountId, db.accounts.id), ); $$AccountsTableProcessedTableManager get accountId { @@ -21510,22 +37655,44 @@ final class $$CalendarSyncStatesTableReferences ); } - static $CalendarSourcesTable _calendarSourceIdTable(_$AppDatabase db) => + static $CalendarSourcesTable _projectionSourceIdTable(_$AppDatabase db) => db.calendarSources.createAlias( $_aliasNameGenerator( - db.calendarSyncStates.calendarSourceId, + db.syncCursors.projectionSourceId, db.calendarSources.id, ), ); - $$CalendarSourcesTableProcessedTableManager? get calendarSourceId { - final $_column = $_itemColumn('calendar_source_id'); + $$CalendarSourcesTableProcessedTableManager? get projectionSourceId { + final $_column = $_itemColumn('projection_source_id'); if ($_column == null) return null; final manager = $$CalendarSourcesTableTableManager( $_db, $_db.calendarSources, ).filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_calendarSourceIdTable($_db)); + final item = $_typedResult.readTableOrNull(_projectionSourceIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $DavCollectionsTable _davCollectionIdTable(_$AppDatabase db) => + db.davCollections.createAlias( + $_aliasNameGenerator( + db.syncCursors.davCollectionId, + db.davCollections.id, + ), + ); + + $$DavCollectionsTableProcessedTableManager? get davCollectionId { + final $_column = $_itemColumn('dav_collection_id'); + if ($_column == null) return null; + final manager = $$DavCollectionsTableTableManager( + $_db, + $_db.davCollections, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_davCollectionIdTable($_db)); if (item == null) return manager; return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item]), @@ -21533,9 +37700,9 @@ final class $$CalendarSyncStatesTableReferences } } -class $$CalendarSyncStatesTableFilterComposer - extends Composer<_$AppDatabase, $CalendarSyncStatesTable> { - $$CalendarSyncStatesTableFilterComposer({ +class $$SyncCursorsTableFilterComposer + extends Composer<_$AppDatabase, $SyncCursorsTable> { + $$SyncCursorsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -21552,8 +37719,23 @@ class $$CalendarSyncStatesTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get syncKind => $composableBuilder( - column: $table.syncKind, + ColumnFilters get transport => $composableBuilder( + column: $table.transport, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get syncScopeKind => $composableBuilder( + column: $table.syncScopeKind, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cursorKind => $composableBuilder( + column: $table.cursorKind, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get cursorValue => $composableBuilder( + column: $table.cursorValue, builder: (column) => ColumnFilters(column), ); @@ -21567,33 +37749,38 @@ class $$CalendarSyncStatesTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get googleSyncToken => $composableBuilder( - column: $table.googleSyncToken, + ColumnFilters get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, builder: (column) => ColumnFilters(column), ); - ColumnFilters get microsoftDeltaLink => $composableBuilder( - column: $table.microsoftDeltaLink, + ColumnFilters get inProgressCursor => $composableBuilder( + column: $table.inProgressCursor, builder: (column) => ColumnFilters(column), ); - ColumnFilters get lastFullSyncAt => $composableBuilder( - column: $table.lastFullSyncAt, + ColumnFilters get inProgressGeneration => $composableBuilder( + column: $table.inProgressGeneration, builder: (column) => ColumnFilters(column), ); - ColumnFilters get lastIncrementalSyncAt => $composableBuilder( - column: $table.lastIncrementalSyncAt, + ColumnFilters get lastCompleteSyncAt => $composableBuilder( + column: $table.lastCompleteSyncAt, builder: (column) => ColumnFilters(column), ); - ColumnFilters get lastError => $composableBuilder( - column: $table.lastError, + ColumnFilters get lastFailureCode => $composableBuilder( + column: $table.lastFailureCode, builder: (column) => ColumnFilters(column), ); - ColumnFilters get rawStateJson => $composableBuilder( - column: $table.rawStateJson, + ColumnFilters get stateSchemaVersion => $composableBuilder( + column: $table.stateSchemaVersion, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get stateJson => $composableBuilder( + column: $table.stateJson, builder: (column) => ColumnFilters(column), ); @@ -21620,10 +37807,10 @@ class $$CalendarSyncStatesTableFilterComposer return composer; } - $$CalendarSourcesTableFilterComposer get calendarSourceId { + $$CalendarSourcesTableFilterComposer get projectionSourceId { final $$CalendarSourcesTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.calendarSourceId, + getCurrentColumn: (t) => t.projectionSourceId, referencedTable: $db.calendarSources, getReferencedColumn: (t) => t.id, builder: @@ -21642,11 +37829,34 @@ class $$CalendarSyncStatesTableFilterComposer ); return composer; } + + $$DavCollectionsTableFilterComposer get davCollectionId { + final $$DavCollectionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableFilterComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } -class $$CalendarSyncStatesTableOrderingComposer - extends Composer<_$AppDatabase, $CalendarSyncStatesTable> { - $$CalendarSyncStatesTableOrderingComposer({ +class $$SyncCursorsTableOrderingComposer + extends Composer<_$AppDatabase, $SyncCursorsTable> { + $$SyncCursorsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -21663,8 +37873,23 @@ class $$CalendarSyncStatesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get syncKind => $composableBuilder( - column: $table.syncKind, + ColumnOrderings get transport => $composableBuilder( + column: $table.transport, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get syncScopeKind => $composableBuilder( + column: $table.syncScopeKind, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cursorKind => $composableBuilder( + column: $table.cursorKind, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get cursorValue => $composableBuilder( + column: $table.cursorValue, builder: (column) => ColumnOrderings(column), ); @@ -21678,33 +37903,38 @@ class $$CalendarSyncStatesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get googleSyncToken => $composableBuilder( - column: $table.googleSyncToken, + ColumnOrderings get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get microsoftDeltaLink => $composableBuilder( - column: $table.microsoftDeltaLink, + ColumnOrderings get inProgressCursor => $composableBuilder( + column: $table.inProgressCursor, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get lastFullSyncAt => $composableBuilder( - column: $table.lastFullSyncAt, + ColumnOrderings get inProgressGeneration => $composableBuilder( + column: $table.inProgressGeneration, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get lastIncrementalSyncAt => $composableBuilder( - column: $table.lastIncrementalSyncAt, + ColumnOrderings get lastCompleteSyncAt => $composableBuilder( + column: $table.lastCompleteSyncAt, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get lastError => $composableBuilder( - column: $table.lastError, + ColumnOrderings get lastFailureCode => $composableBuilder( + column: $table.lastFailureCode, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get stateSchemaVersion => $composableBuilder( + column: $table.stateSchemaVersion, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get rawStateJson => $composableBuilder( - column: $table.rawStateJson, + ColumnOrderings get stateJson => $composableBuilder( + column: $table.stateJson, builder: (column) => ColumnOrderings(column), ); @@ -21731,10 +37961,10 @@ class $$CalendarSyncStatesTableOrderingComposer return composer; } - $$CalendarSourcesTableOrderingComposer get calendarSourceId { + $$CalendarSourcesTableOrderingComposer get projectionSourceId { final $$CalendarSourcesTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.calendarSourceId, + getCurrentColumn: (t) => t.projectionSourceId, referencedTable: $db.calendarSources, getReferencedColumn: (t) => t.id, builder: @@ -21753,11 +37983,34 @@ class $$CalendarSyncStatesTableOrderingComposer ); return composer; } + + $$DavCollectionsTableOrderingComposer get davCollectionId { + final $$DavCollectionsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableOrderingComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } -class $$CalendarSyncStatesTableAnnotationComposer - extends Composer<_$AppDatabase, $CalendarSyncStatesTable> { - $$CalendarSyncStatesTableAnnotationComposer({ +class $$SyncCursorsTableAnnotationComposer + extends Composer<_$AppDatabase, $SyncCursorsTable> { + $$SyncCursorsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -21770,8 +38023,23 @@ class $$CalendarSyncStatesTableAnnotationComposer GeneratedColumn get provider => $composableBuilder(column: $table.provider, builder: (column) => column); - GeneratedColumn get syncKind => - $composableBuilder(column: $table.syncKind, builder: (column) => column); + GeneratedColumn get transport => + $composableBuilder(column: $table.transport, builder: (column) => column); + + GeneratedColumn get syncScopeKind => $composableBuilder( + column: $table.syncScopeKind, + builder: (column) => column, + ); + + GeneratedColumn get cursorKind => $composableBuilder( + column: $table.cursorKind, + builder: (column) => column, + ); + + GeneratedColumn get cursorValue => $composableBuilder( + column: $table.cursorValue, + builder: (column) => column, + ); GeneratedColumn get rangeStart => $composableBuilder( column: $table.rangeStart, @@ -21781,34 +38049,39 @@ class $$CalendarSyncStatesTableAnnotationComposer GeneratedColumn get rangeEnd => $composableBuilder(column: $table.rangeEnd, builder: (column) => column); - GeneratedColumn get googleSyncToken => $composableBuilder( - column: $table.googleSyncToken, + GeneratedColumn get baselineGeneration => $composableBuilder( + column: $table.baselineGeneration, builder: (column) => column, ); - GeneratedColumn get microsoftDeltaLink => $composableBuilder( - column: $table.microsoftDeltaLink, + GeneratedColumn get inProgressCursor => $composableBuilder( + column: $table.inProgressCursor, builder: (column) => column, ); - GeneratedColumn get lastFullSyncAt => $composableBuilder( - column: $table.lastFullSyncAt, + GeneratedColumn get inProgressGeneration => $composableBuilder( + column: $table.inProgressGeneration, builder: (column) => column, ); - GeneratedColumn get lastIncrementalSyncAt => $composableBuilder( - column: $table.lastIncrementalSyncAt, + GeneratedColumn get lastCompleteSyncAt => $composableBuilder( + column: $table.lastCompleteSyncAt, builder: (column) => column, ); - GeneratedColumn get lastError => - $composableBuilder(column: $table.lastError, builder: (column) => column); + GeneratedColumn get lastFailureCode => $composableBuilder( + column: $table.lastFailureCode, + builder: (column) => column, + ); - GeneratedColumn get rawStateJson => $composableBuilder( - column: $table.rawStateJson, + GeneratedColumn get stateSchemaVersion => $composableBuilder( + column: $table.stateSchemaVersion, builder: (column) => column, ); + GeneratedColumn get stateJson => + $composableBuilder(column: $table.stateJson, builder: (column) => column); + $$AccountsTableAnnotationComposer get accountId { final $$AccountsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -21832,10 +38105,10 @@ class $$CalendarSyncStatesTableAnnotationComposer return composer; } - $$CalendarSourcesTableAnnotationComposer get calendarSourceId { + $$CalendarSourcesTableAnnotationComposer get projectionSourceId { final $$CalendarSourcesTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.calendarSourceId, + getCurrentColumn: (t) => t.projectionSourceId, referencedTable: $db.calendarSources, getReferencedColumn: (t) => t.id, builder: @@ -21854,113 +38127,159 @@ class $$CalendarSyncStatesTableAnnotationComposer ); return composer; } + + $$DavCollectionsTableAnnotationComposer get davCollectionId { + final $$DavCollectionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.davCollectionId, + referencedTable: $db.davCollections, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$DavCollectionsTableAnnotationComposer( + $db: $db, + $table: $db.davCollections, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } -class $$CalendarSyncStatesTableTableManager +class $$SyncCursorsTableTableManager extends RootTableManager< _$AppDatabase, - $CalendarSyncStatesTable, - CalendarSyncState, - $$CalendarSyncStatesTableFilterComposer, - $$CalendarSyncStatesTableOrderingComposer, - $$CalendarSyncStatesTableAnnotationComposer, - $$CalendarSyncStatesTableCreateCompanionBuilder, - $$CalendarSyncStatesTableUpdateCompanionBuilder, - (CalendarSyncState, $$CalendarSyncStatesTableReferences), - CalendarSyncState, - PrefetchHooks Function({bool accountId, bool calendarSourceId}) + $SyncCursorsTable, + SyncCursor, + $$SyncCursorsTableFilterComposer, + $$SyncCursorsTableOrderingComposer, + $$SyncCursorsTableAnnotationComposer, + $$SyncCursorsTableCreateCompanionBuilder, + $$SyncCursorsTableUpdateCompanionBuilder, + (SyncCursor, $$SyncCursorsTableReferences), + SyncCursor, + PrefetchHooks Function({ + bool accountId, + bool projectionSourceId, + bool davCollectionId, + }) > { - $$CalendarSyncStatesTableTableManager( - _$AppDatabase db, - $CalendarSyncStatesTable table, - ) : super( + $$SyncCursorsTableTableManager(_$AppDatabase db, $SyncCursorsTable table) + : super( TableManagerState( db: db, table: table, createFilteringComposer: () => - $$CalendarSyncStatesTableFilterComposer($db: db, $table: table), + $$SyncCursorsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$CalendarSyncStatesTableOrderingComposer($db: db, $table: table), + $$SyncCursorsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$CalendarSyncStatesTableAnnotationComposer( - $db: db, - $table: table, - ), + $$SyncCursorsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value accountId = const Value.absent(), - Value calendarSourceId = const Value.absent(), + Value projectionSourceId = const Value.absent(), Value provider = const Value.absent(), - Value syncKind = const Value.absent(), + Value transport = const Value.absent(), + Value syncScopeKind = const Value.absent(), + Value davCollectionId = const Value.absent(), + Value cursorKind = const Value.absent(), + Value cursorValue = const Value.absent(), Value rangeStart = const Value.absent(), Value rangeEnd = const Value.absent(), - Value googleSyncToken = const Value.absent(), - Value microsoftDeltaLink = const Value.absent(), - Value lastFullSyncAt = const Value.absent(), - Value lastIncrementalSyncAt = const Value.absent(), - Value lastError = const Value.absent(), - Value rawStateJson = const Value.absent(), + Value baselineGeneration = const Value.absent(), + Value inProgressCursor = const Value.absent(), + Value inProgressGeneration = const Value.absent(), + Value lastCompleteSyncAt = const Value.absent(), + Value lastFailureCode = const Value.absent(), + Value stateSchemaVersion = const Value.absent(), + Value stateJson = const Value.absent(), Value rowid = const Value.absent(), - }) => CalendarSyncStatesCompanion( + }) => SyncCursorsCompanion( id: id, accountId: accountId, - calendarSourceId: calendarSourceId, + projectionSourceId: projectionSourceId, provider: provider, - syncKind: syncKind, + transport: transport, + syncScopeKind: syncScopeKind, + davCollectionId: davCollectionId, + cursorKind: cursorKind, + cursorValue: cursorValue, rangeStart: rangeStart, rangeEnd: rangeEnd, - googleSyncToken: googleSyncToken, - microsoftDeltaLink: microsoftDeltaLink, - lastFullSyncAt: lastFullSyncAt, - lastIncrementalSyncAt: lastIncrementalSyncAt, - lastError: lastError, - rawStateJson: rawStateJson, + baselineGeneration: baselineGeneration, + inProgressCursor: inProgressCursor, + inProgressGeneration: inProgressGeneration, + lastCompleteSyncAt: lastCompleteSyncAt, + lastFailureCode: lastFailureCode, + stateSchemaVersion: stateSchemaVersion, + stateJson: stateJson, rowid: rowid, ), createCompanionCallback: ({ required String id, required String accountId, - Value calendarSourceId = const Value.absent(), + Value projectionSourceId = const Value.absent(), required String provider, - required String syncKind, + required String transport, + required String syncScopeKind, + Value davCollectionId = const Value.absent(), + required String cursorKind, + required String cursorValue, Value rangeStart = const Value.absent(), Value rangeEnd = const Value.absent(), - Value googleSyncToken = const Value.absent(), - Value microsoftDeltaLink = const Value.absent(), - Value lastFullSyncAt = const Value.absent(), - Value lastIncrementalSyncAt = const Value.absent(), - Value lastError = const Value.absent(), - Value rawStateJson = const Value.absent(), + Value baselineGeneration = const Value.absent(), + Value inProgressCursor = const Value.absent(), + Value inProgressGeneration = const Value.absent(), + Value lastCompleteSyncAt = const Value.absent(), + Value lastFailureCode = const Value.absent(), + Value stateSchemaVersion = const Value.absent(), + Value stateJson = const Value.absent(), Value rowid = const Value.absent(), - }) => CalendarSyncStatesCompanion.insert( + }) => SyncCursorsCompanion.insert( id: id, accountId: accountId, - calendarSourceId: calendarSourceId, + projectionSourceId: projectionSourceId, provider: provider, - syncKind: syncKind, + transport: transport, + syncScopeKind: syncScopeKind, + davCollectionId: davCollectionId, + cursorKind: cursorKind, + cursorValue: cursorValue, rangeStart: rangeStart, rangeEnd: rangeEnd, - googleSyncToken: googleSyncToken, - microsoftDeltaLink: microsoftDeltaLink, - lastFullSyncAt: lastFullSyncAt, - lastIncrementalSyncAt: lastIncrementalSyncAt, - lastError: lastError, - rawStateJson: rawStateJson, + baselineGeneration: baselineGeneration, + inProgressCursor: inProgressCursor, + inProgressGeneration: inProgressGeneration, + lastCompleteSyncAt: lastCompleteSyncAt, + lastFailureCode: lastFailureCode, + stateSchemaVersion: stateSchemaVersion, + stateJson: stateJson, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map( (e) => ( e.readTable(table), - $$CalendarSyncStatesTableReferences(db, table, e), + $$SyncCursorsTableReferences(db, table, e), ), ) .toList(), prefetchHooksCallback: - ({accountId = false, calendarSourceId = false}) { + ({ + accountId = false, + projectionSourceId = false, + davCollectionId = false, + }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -21986,26 +38305,41 @@ class $$CalendarSyncStatesTableTableManager currentTable: table, currentColumn: table.accountId, referencedTable: - $$CalendarSyncStatesTableReferences + $$SyncCursorsTableReferences ._accountIdTable(db), referencedColumn: - $$CalendarSyncStatesTableReferences + $$SyncCursorsTableReferences ._accountIdTable(db) .id, ) as T; } - if (calendarSourceId) { + if (projectionSourceId) { state = state.withJoin( currentTable: table, - currentColumn: table.calendarSourceId, + currentColumn: table.projectionSourceId, referencedTable: - $$CalendarSyncStatesTableReferences - ._calendarSourceIdTable(db), + $$SyncCursorsTableReferences + ._projectionSourceIdTable(db), referencedColumn: - $$CalendarSyncStatesTableReferences - ._calendarSourceIdTable(db) + $$SyncCursorsTableReferences + ._projectionSourceIdTable(db) + .id, + ) + as T; + } + if (davCollectionId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.davCollectionId, + referencedTable: + $$SyncCursorsTableReferences + ._davCollectionIdTable(db), + referencedColumn: + $$SyncCursorsTableReferences + ._davCollectionIdTable(db) .id, ) as T; @@ -22022,19 +38356,23 @@ class $$CalendarSyncStatesTableTableManager ); } -typedef $$CalendarSyncStatesTableProcessedTableManager = +typedef $$SyncCursorsTableProcessedTableManager = ProcessedTableManager< _$AppDatabase, - $CalendarSyncStatesTable, - CalendarSyncState, - $$CalendarSyncStatesTableFilterComposer, - $$CalendarSyncStatesTableOrderingComposer, - $$CalendarSyncStatesTableAnnotationComposer, - $$CalendarSyncStatesTableCreateCompanionBuilder, - $$CalendarSyncStatesTableUpdateCompanionBuilder, - (CalendarSyncState, $$CalendarSyncStatesTableReferences), - CalendarSyncState, - PrefetchHooks Function({bool accountId, bool calendarSourceId}) + $SyncCursorsTable, + SyncCursor, + $$SyncCursorsTableFilterComposer, + $$SyncCursorsTableOrderingComposer, + $$SyncCursorsTableAnnotationComposer, + $$SyncCursorsTableCreateCompanionBuilder, + $$SyncCursorsTableUpdateCompanionBuilder, + (SyncCursor, $$SyncCursorsTableReferences), + SyncCursor, + PrefetchHooks Function({ + bool accountId, + bool projectionSourceId, + bool davCollectionId, + }) >; typedef $$CalendarColorsTableCreateCompanionBuilder = CalendarColorsCompanion Function({ @@ -23140,6 +39478,16 @@ class $AppDatabaseManager { $AppDatabaseManager(this._db); $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db, _db.accounts); + $$DavAccountServicesTableTableManager get davAccountServices => + $$DavAccountServicesTableTableManager(_db, _db.davAccountServices); + $$DavCollectionsTableTableManager get davCollections => + $$DavCollectionsTableTableManager(_db, _db.davCollections); + $$DavObjectsTableTableManager get davObjects => + $$DavObjectsTableTableManager(_db, _db.davObjects); + $$DavObjectComponentsTableTableManager get davObjectComponents => + $$DavObjectComponentsTableTableManager(_db, _db.davObjectComponents); + $$DavConflictSnapshotsTableTableManager get davConflictSnapshots => + $$DavConflictSnapshotsTableTableManager(_db, _db.davConflictSnapshots); $$TaskListsTableTableManager get taskLists => $$TaskListsTableTableManager(_db, _db.taskLists); $$TasksTableTableManager get tasks => @@ -23162,8 +39510,8 @@ class $AppDatabaseManager { _db, _db.calendarEventReminders, ); - $$CalendarSyncStatesTableTableManager get calendarSyncStates => - $$CalendarSyncStatesTableTableManager(_db, _db.calendarSyncStates); + $$SyncCursorsTableTableManager get syncCursors => + $$SyncCursorsTableTableManager(_db, _db.syncCursors); $$CalendarColorsTableTableManager get calendarColors => $$CalendarColorsTableTableManager(_db, _db.calendarColors); $$ScheduleItemOverridesTableTableManager get scheduleItemOverrides => @@ -23174,6 +39522,7 @@ class $AppDatabaseManager { mixin _$TaskListsDaoMixin on DatabaseAccessor { $AccountsTable get accounts => attachedDatabase.accounts; + $DavCollectionsTable get davCollections => attachedDatabase.davCollections; $TaskListsTable get taskLists => attachedDatabase.taskLists; TaskListsDaoManager get managers => TaskListsDaoManager(this); } @@ -23183,13 +39532,22 @@ class TaskListsDaoManager { TaskListsDaoManager(this._db); $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db.attachedDatabase, _db.accounts); + $$DavCollectionsTableTableManager get davCollections => + $$DavCollectionsTableTableManager( + _db.attachedDatabase, + _db.davCollections, + ); $$TaskListsTableTableManager get taskLists => $$TaskListsTableTableManager(_db.attachedDatabase, _db.taskLists); } mixin _$TasksDaoMixin on DatabaseAccessor { $AccountsTable get accounts => attachedDatabase.accounts; + $DavCollectionsTable get davCollections => attachedDatabase.davCollections; $TaskListsTable get taskLists => attachedDatabase.taskLists; + $DavObjectsTable get davObjects => attachedDatabase.davObjects; + $DavObjectComponentsTable get davObjectComponents => + attachedDatabase.davObjectComponents; $TasksTable get tasks => attachedDatabase.tasks; TasksDaoManager get managers => TasksDaoManager(this); } @@ -23199,14 +39557,30 @@ class TasksDaoManager { TasksDaoManager(this._db); $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db.attachedDatabase, _db.accounts); + $$DavCollectionsTableTableManager get davCollections => + $$DavCollectionsTableTableManager( + _db.attachedDatabase, + _db.davCollections, + ); $$TaskListsTableTableManager get taskLists => $$TaskListsTableTableManager(_db.attachedDatabase, _db.taskLists); + $$DavObjectsTableTableManager get davObjects => + $$DavObjectsTableTableManager(_db.attachedDatabase, _db.davObjects); + $$DavObjectComponentsTableTableManager get davObjectComponents => + $$DavObjectComponentsTableTableManager( + _db.attachedDatabase, + _db.davObjectComponents, + ); $$TasksTableTableManager get tasks => $$TasksTableTableManager(_db.attachedDatabase, _db.tasks); } mixin _$PendingOpsDaoMixin on DatabaseAccessor { $AccountsTable get accounts => attachedDatabase.accounts; + $DavCollectionsTable get davCollections => attachedDatabase.davCollections; + $DavObjectsTable get davObjects => attachedDatabase.davObjects; + $DavConflictSnapshotsTable get davConflictSnapshots => + attachedDatabase.davConflictSnapshots; $PendingOpsTable get pendingOps => attachedDatabase.pendingOps; PendingOpsDaoManager get managers => PendingOpsDaoManager(this); } @@ -23216,6 +39590,18 @@ class PendingOpsDaoManager { PendingOpsDaoManager(this._db); $$AccountsTableTableManager get accounts => $$AccountsTableTableManager(_db.attachedDatabase, _db.accounts); + $$DavCollectionsTableTableManager get davCollections => + $$DavCollectionsTableTableManager( + _db.attachedDatabase, + _db.davCollections, + ); + $$DavObjectsTableTableManager get davObjects => + $$DavObjectsTableTableManager(_db.attachedDatabase, _db.davObjects); + $$DavConflictSnapshotsTableTableManager get davConflictSnapshots => + $$DavConflictSnapshotsTableTableManager( + _db.attachedDatabase, + _db.davConflictSnapshots, + ); $$PendingOpsTableTableManager get pendingOps => $$PendingOpsTableTableManager(_db.attachedDatabase, _db.pendingOps); } diff --git a/lib/src/db/daos/task_lists_dao.dart b/lib/src/db/daos/task_lists_dao.dart index 2f178a0..9671695 100644 --- a/lib/src/db/daos/task_lists_dao.dart +++ b/lib/src/db/daos/task_lists_dao.dart @@ -1,27 +1,48 @@ part of '../app_database.dart'; -@DriftAccessor(tables: [TaskLists]) +@DriftAccessor(tables: [TaskLists, DavCollections]) class TaskListsDao extends DatabaseAccessor with _$TaskListsDaoMixin { TaskListsDao(super.db); Stream> watchTaskLists(String accountId) { - final query = select(taskLists) - ..where( - (row) => - row.accountId.equals(accountId) & - row.pendingDelete.equals(false) & - row.serverMissing.equals(false), - ) - ..orderBy([(row) => OrderingTerm.asc(row.title)]); - return query.watch(); + final query = + select(taskLists).join([ + leftOuterJoin( + davCollections, + davCollections.id.equalsExp(taskLists.davCollectionId), + ), + ]) + ..where( + taskLists.accountId.equals(accountId) & + taskLists.pendingDelete.equals(false) & + taskLists.serverMissing.equals(false) & + (taskLists.davCollectionId.isNull() | + davCollections.tasksSelected.equals(true)), + ) + ..orderBy([OrderingTerm.asc(taskLists.title)]); + return query.watch().map( + (rows) => [for (final row in rows) row.readTable(taskLists)], + ); } Future> listTaskLists(String accountId) { - final query = select(taskLists) - ..where((row) => row.accountId.equals(accountId)) - ..orderBy([(row) => OrderingTerm.asc(row.title)]); - return query.get(); + final query = + select(taskLists).join([ + leftOuterJoin( + davCollections, + davCollections.id.equalsExp(taskLists.davCollectionId), + ), + ]) + ..where( + taskLists.accountId.equals(accountId) & + (taskLists.davCollectionId.isNull() | + davCollections.tasksSelected.equals(true)), + ) + ..orderBy([OrderingTerm.asc(taskLists.title)]); + return query.get().then( + (rows) => [for (final row in rows) row.readTable(taskLists)], + ); } Future upsertTaskList(TaskListsCompanion row) { diff --git a/lib/src/db/daos/tasks_dao.dart b/lib/src/db/daos/tasks_dao.dart index 3afa383..7f6327e 100644 --- a/lib/src/db/daos/tasks_dao.dart +++ b/lib/src/db/daos/tasks_dao.dart @@ -1,24 +1,38 @@ part of '../app_database.dart'; -@DriftAccessor(tables: [Accounts, TaskLists, Tasks]) +@DriftAccessor(tables: [Accounts, DavCollections, TaskLists, Tasks]) class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { TasksDao(super.db); Stream> watchTaskTree(String accountId, String taskListId) { - final query = select(tasks) - ..where( - (row) => - row.accountId.equals(accountId) & - row.taskListId.equals(taskListId) & - row.pendingDelete.equals(false) & - row.serverMissing.equals(false), - ) - ..orderBy([ - (row) => OrderingTerm.asc(row.parent), - (row) => OrderingTerm.asc(row.position), - (row) => OrderingTerm.asc(row.title), - ]); - return query.watch(); + final query = + select(tasks).join([ + innerJoin( + taskLists, + taskLists.accountId.equalsExp(tasks.accountId) & + taskLists.id.equalsExp(tasks.taskListId), + ), + leftOuterJoin( + davCollections, + davCollections.id.equalsExp(taskLists.davCollectionId), + ), + ]) + ..where( + tasks.accountId.equals(accountId) & + tasks.taskListId.equals(taskListId) & + tasks.pendingDelete.equals(false) & + tasks.serverMissing.equals(false) & + (taskLists.davCollectionId.isNull() | + davCollections.tasksSelected.equals(true)), + ) + ..orderBy([ + OrderingTerm.asc(tasks.parent), + OrderingTerm.asc(tasks.position), + OrderingTerm.asc(tasks.title), + ]); + return query.watch().map( + (rows) => [for (final row in rows) row.readTable(tasks)], + ); } Stream> watchAllTaskTrees(List accountIds) { @@ -34,6 +48,10 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { taskLists.id.equalsExp(tasks.taskListId), ), innerJoin(accounts, accounts.id.equalsExp(tasks.accountId)), + leftOuterJoin( + davCollections, + davCollections.id.equalsExp(taskLists.davCollectionId), + ), ]) ..where( tasks.accountId.isIn(accountIds) & @@ -41,7 +59,15 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { tasks.serverMissing.equals(false) & taskLists.pendingDelete.equals(false) & taskLists.serverMissing.equals(false) & - accounts.authState.equals('signed_in'), + (taskLists.davCollectionId.isNull() | + davCollections.tasksSelected.equals(true)) & + accounts.authState.isIn(const [ + 'signed_in', + 'reauth_required', + 'temporarily_unavailable', + 'permission_changed', + 'unsupported_server_profile', + ]), ) ..orderBy([ OrderingTerm.asc(accounts.provider), diff --git a/lib/src/db/migrations.dart b/lib/src/db/migrations.dart index d60353a..724ea3e 100644 --- a/lib/src/db/migrations.dart +++ b/lib/src/db/migrations.dart @@ -2,13 +2,26 @@ import 'package:drift/drift.dart'; import 'app_database.dart'; -const latestSchemaVersion = 5; +const latestSchemaVersion = 8; + +/// A recoverable, non-secret diagnostic raised when an on-disk schema cannot +/// be migrated without guessing remote identity or losing synchronized data. +final class BusyMaxMigrationException implements Exception { + const BusyMaxMigrationException(this.code, this.message); + + final String code; + final String message; + + @override + String toString() => 'BusyMaxMigrationException($code: $message)'; +} MigrationStrategy busyMaxMigrationStrategy(AppDatabase database) { return MigrationStrategy( onCreate: (migrator) async { await migrator.createAll(); await _createIndexes(database); + await _verifyForeignKeys(database); }, onUpgrade: (migrator, from, to) async { if (from < 2 && await _hasTable(database, 'pending_ops')) { @@ -26,7 +39,17 @@ MigrationStrategy busyMaxMigrationStrategy(AppDatabase database) { if (from >= 4 && from < 5) { await _addV5CalendarEventCategories(migrator, database); } + if (from < 6) { + await _migrateToV6(migrator, database); + } + if (from < 7) { + await _migrateToV7(migrator, database); + } + if (from < 8) { + await _migrateToV8(migrator, database); + } await _createIndexes(database); + await _verifyForeignKeys(database); }, beforeOpen: (details) async { await database.customStatement('PRAGMA foreign_keys = ON'); @@ -34,6 +57,351 @@ MigrationStrategy busyMaxMigrationStrategy(AppDatabase database) { ); } +Future _migrateToV8(Migrator migrator, AppDatabase database) async { + await _addColumnIfMissing( + migrator, + database, + database.tasks, + database.tasks.microsoftChecklistItemsJson, + ); +} + +Future _migrateToV7(Migrator migrator, AppDatabase database) async { + if (!await _hasTable(database, 'tasks')) return; + for (final column in [ + database.tasks.taskLocation, + database.tasks.taskUrl, + database.tasks.taskClassification, + database.tasks.taskPinned, + database.tasks.taskHideSubtasks, + database.tasks.taskHideCompletedSubtasks, + database.tasks.taskAlarmsJson, + ]) { + await _addColumnIfMissing(migrator, database, database.tasks, column); + } +} + +Future _migrateToV6(Migrator migrator, AppDatabase database) async { + final preservedCounts = await _capturePreservedCounts(database); + await _validateLegacyProviderValues(database); + + // The v5 index is intentionally removed before Drift's table rebuild so it + // cannot be recreated against the new authority-aware account identity. + await database.customStatement( + 'DROP INDEX IF EXISTS idx_accounts_provider_account', + ); + await migrator.alterTable( + TableMigration( + database.accounts, + newColumns: [ + database.accounts.authority, + database.accounts.credentialKind, + database.accounts.providerProfileVersion, + ], + columnTransformer: { + database.accounts.provider: const CustomExpression('provider'), + database.accounts.authority: const CustomExpression( + 'CASE provider ' + "WHEN 'google' THEN 'https://accounts.google.com' " + "WHEN 'microsoft' THEN 'https://login.microsoftonline.com/' || " + "lower(COALESCE(NULLIF(trim(tenant_id), ''), 'common')) " + 'END', + ), + database.accounts.providerAccountId: const CustomExpression( + "COALESCE(NULLIF(trim(provider_account_id), ''), id)", + ), + database.accounts.credentialKind: const CustomExpression( + "'oauth'", + ), + database.accounts.providerProfileVersion: const CustomExpression( + '1', + ), + }, + ), + ); + + await migrator.createTable(database.davAccountServices); + await migrator.createTable(database.davCollections); + await migrator.createTable(database.davObjects); + await migrator.createTable(database.davObjectComponents); + await migrator.createTable(database.davConflictSnapshots); + + await _addProjectionLinks(migrator, database); + await _addDavPendingOperationColumns(migrator, database); + await _migrateGenericCursors(migrator, database); + + await _verifyPreservedCounts(database, preservedCounts); + await _verifyV6AccountIdentity(database); +} + +Future _addProjectionLinks( + Migrator migrator, + AppDatabase database, +) async { + await _addColumnIfMissing( + migrator, + database, + database.taskLists, + database.taskLists.davCollectionId, + ); + for (final column in [ + database.tasks.davCollectionId, + database.tasks.davObjectId, + database.tasks.davComponentId, + database.tasks.icalUid, + database.tasks.recurrenceIdKey, + database.tasks.icalPriority, + database.tasks.percentComplete, + database.tasks.parentUid, + database.tasks.sortOrder, + database.tasks.providerExtensionProjectionJson, + database.tasks.projectionVersion, + ]) { + await _addColumnIfMissing(migrator, database, database.tasks, column); + } + await _addColumnIfMissing( + migrator, + database, + database.calendarSources, + database.calendarSources.davCollectionId, + ); + for (final column in [ + database.calendarEvents.davCollectionId, + database.calendarEvents.davObjectId, + database.calendarEvents.davComponentId, + database.calendarEvents.icalUid, + database.calendarEvents.recurrenceIdKey, + database.calendarEvents.occurrenceKey, + database.calendarEvents.projectionVersion, + ]) { + await _addColumnIfMissing( + migrator, + database, + database.calendarEvents, + column, + ); + } +} + +Future _addDavPendingOperationColumns( + Migrator migrator, + AppDatabase database, +) async { + for (final column in [ + database.pendingOps.davCollectionId, + database.pendingOps.davCollectionHref, + database.pendingOps.davObjectId, + database.pendingOps.davMemberHref, + database.pendingOps.baselineEtag, + database.pendingOps.baselineRawIcs, + database.pendingOps.mutationPatchJson, + database.pendingOps.mutationPatchSchemaVersion, + database.pendingOps.targetComponentKey, + database.pendingOps.mutationScope, + database.pendingOps.destinationCollectionId, + database.pendingOps.destinationCollectionHref, + database.pendingOps.destinationMemberHref, + database.pendingOps.conflictState, + database.pendingOps.conflictSnapshotId, + database.pendingOps.retryClassification, + ]) { + await _addColumnIfMissing(migrator, database, database.pendingOps, column); + } +} + +Future _migrateGenericCursors( + Migrator migrator, + AppDatabase database, +) async { + await migrator.createTable(database.syncCursors); + if (!await _hasTable(database, 'calendar_sync_states')) { + return; + } + + await database.customStatement(''' + INSERT INTO sync_cursors ( + id, + account_id, + projection_source_id, + provider, + transport, + sync_scope_kind, + dav_collection_id, + cursor_kind, + cursor_value, + range_start, + range_end, + baseline_generation, + in_progress_cursor, + in_progress_generation, + last_complete_sync_at, + last_failure_code, + state_schema_version, + state_json + ) + SELECT + id, + account_id, + calendar_source_id, + provider, + 'rest', + sync_kind, + NULL, + CASE provider + WHEN 'google' THEN + CASE WHEN google_sync_token IS NULL + THEN 'snapshot_generation' ELSE 'google_sync_token' END + WHEN 'microsoft' THEN + CASE WHEN microsoft_delta_link IS NULL + THEN 'snapshot_generation' ELSE 'microsoft_delta_link' END + END, + COALESCE(google_sync_token, microsoft_delta_link, '0'), + range_start, + range_end, + 0, + NULL, + NULL, + CASE + WHEN last_incremental_sync_at IS NULL THEN last_full_sync_at + WHEN last_full_sync_at IS NULL THEN last_incremental_sync_at + WHEN last_incremental_sync_at >= last_full_sync_at + THEN last_incremental_sync_at + ELSE last_full_sync_at + END, + last_error, + 1, + raw_state_json + FROM calendar_sync_states + '''); + await migrator.deleteTable('calendar_sync_states'); +} + +Future _validateLegacyProviderValues(AppDatabase database) async { + const providerColumns = <(String, String, bool)>[ + ('accounts', 'provider', false), + ('pending_ops', 'provider', true), + ('sync_runs', 'provider', true), + ('calendar_sources', 'provider', false), + ('calendar_events', 'provider', false), + ('calendar_event_reminders', 'provider', false), + ('calendar_colors', 'provider', false), + ('calendar_sync_states', 'provider', false), + ]; + for (final (table, column, nullable) in providerColumns) { + if (!await _hasTable(database, table) || + !await _hasColumn(database, table, column)) { + continue; + } + final rows = await database + .customSelect( + 'SELECT DISTINCT "$column" AS provider_value FROM "$table"', + ) + .get(); + for (final row in rows) { + final value = row.readNullable('provider_value'); + if (nullable && value == null) { + continue; + } + if (value != 'google' && value != 'microsoft') { + throw BusyMaxMigrationException( + 'unsupported_provider_value', + 'Schema 5 contains an unsupported provider value in $table.', + ); + } + } + } +} + +Future> _capturePreservedCounts(AppDatabase database) async { + const tables = [ + 'accounts', + 'task_lists', + 'tasks', + 'pending_ops', + 'sync_runs', + 'calendar_sources', + 'calendar_events', + 'calendar_event_attendees', + 'calendar_event_reminders', + 'calendar_colors', + 'schedule_item_overrides', + 'notification_schedule', + ]; + final counts = {}; + for (final table in tables) { + if (await _hasTable(database, table)) { + counts[table] = await _tableCount(database, table); + } + } + if (await _hasTable(database, 'calendar_sync_states')) { + counts['sync_cursors'] = await _tableCount( + database, + 'calendar_sync_states', + ); + } + return counts; +} + +Future _verifyPreservedCounts( + AppDatabase database, + Map expected, +) async { + for (final entry in expected.entries) { + final actual = await _tableCount(database, entry.key); + if (actual != entry.value) { + throw BusyMaxMigrationException( + 'row_count_invariant_failed', + 'Migration row-count invariant failed for ${entry.key}.', + ); + } + } +} + +Future _verifyV6AccountIdentity(AppDatabase database) async { + final invalid = await database.customSelect(''' + SELECT id FROM accounts + WHERE provider NOT IN ('google', 'microsoft', 'apple_icloud', 'nextcloud') + OR trim(authority) = '' + OR trim(provider_account_id) = '' + OR credential_kind NOT IN ( + 'oauth', 'apple_app_specific_password', 'nextcloud_app_password' + ) + LIMIT 1 + ''').getSingleOrNull(); + if (invalid != null) { + throw const BusyMaxMigrationException( + 'account_identity_invariant_failed', + 'An account could not be assigned a strict remote identity.', + ); + } + + final duplicate = await database.customSelect(''' + SELECT provider, authority, provider_account_id + FROM accounts + GROUP BY provider, authority, provider_account_id + HAVING count(*) > 1 + LIMIT 1 + ''').getSingleOrNull(); + if (duplicate != null) { + throw const BusyMaxMigrationException( + 'duplicate_remote_account_identity', + 'Multiple accounts map to the same provider authority and account ID.', + ); + } +} + +Future _verifyForeignKeys(AppDatabase database) async { + final violations = await database + .customSelect('PRAGMA foreign_key_check') + .get(); + if (violations.isNotEmpty) { + throw const BusyMaxMigrationException( + 'foreign_key_check_failed', + 'The migrated database contains invalid foreign-key references.', + ); + } +} + Future _addV4CalendarTables( Migrator migrator, AppDatabase database, @@ -69,7 +437,7 @@ Future _addV4CalendarTables( await migrator.createTable(database.calendarEvents); await migrator.createTable(database.calendarEventAttendees); await migrator.createTable(database.calendarEventReminders); - await migrator.createTable(database.calendarSyncStates); + await database.customStatement(_legacyCalendarSyncStatesSql); await migrator.createTable(database.calendarColors); await migrator.createTable(database.scheduleItemOverrides); await migrator.createTable(database.notificationSchedule); @@ -89,10 +457,13 @@ Future _addV5CalendarEventCategories( Future _addV3Columns(Migrator migrator, AppDatabase database) async { if (await _hasTable(database, 'accounts')) { - await migrator.addColumn(database.accounts, database.accounts.provider); - await migrator.addColumn( - database.accounts, - database.accounts.providerAccountId, + // These statements deliberately recreate the historical nullable/defaulted + // v3 shape. The v6 table rebuild below then validates and tightens it. + await database.customStatement( + "ALTER TABLE accounts ADD COLUMN provider TEXT NOT NULL DEFAULT 'google'", + ); + await database.customStatement( + 'ALTER TABLE accounts ADD COLUMN provider_account_id TEXT NULL', ); await migrator.addColumn(database.accounts, database.accounts.email); await migrator.addColumn(database.accounts, database.accounts.tenantId); @@ -121,53 +492,27 @@ Future _addV3Columns(Migrator migrator, AppDatabase database) async { } if (await _hasTable(database, 'tasks')) { - await migrator.addColumn(database.tasks, database.tasks.providerStatus); - await migrator.addColumn(database.tasks, database.tasks.bodyContent); - await migrator.addColumn(database.tasks, database.tasks.bodyContentType); - await migrator.addColumn( - database.tasks, + for (final column in [ + database.tasks.providerStatus, + database.tasks.bodyContent, + database.tasks.bodyContentType, database.tasks.microsoftDueDateTime, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftDueTimeZone, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftStartDateTime, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftStartTimeZone, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftReminderDateTime, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftReminderTimeZone, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftIsReminderOn, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftCompletedDateTime, - ); - await migrator.addColumn( - database.tasks, database.tasks.microsoftCompletedTimeZone, - ); - await migrator.addColumn(database.tasks, database.tasks.recurrenceJson); - await migrator.addColumn(database.tasks, database.tasks.importance); - await migrator.addColumn(database.tasks, database.tasks.categoriesJson); - await migrator.addColumn(database.tasks, database.tasks.hasAttachments); - await migrator.addColumn( - database.tasks, + database.tasks.recurrenceJson, + database.tasks.importance, + database.tasks.categoriesJson, + database.tasks.hasAttachments, database.tasks.providerMetadataJson, - ); + ]) { + await migrator.addColumn(database.tasks, column); + } } if (await _hasTable(database, 'pending_ops')) { @@ -184,10 +529,43 @@ Future _createIndexes(AppDatabase database) async { 'CREATE INDEX IF NOT EXISTS idx_accounts_provider ' 'ON accounts(provider)', ); + if (await _hasColumn(database, 'accounts', 'authority')) { + await database.customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_remote_identity ' + 'ON accounts(provider, authority, provider_account_id)', + ); + } + } + if (await _hasTable(database, 'dav_collections')) { + await database.customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_dav_collections_href ' + 'ON dav_collections(account_id, href_key)', + ); + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_dav_collections_sync ' + 'ON dav_collections(account_id, deleted, server_missing, last_sync_at_utc)', + ); + } + if (await _hasTable(database, 'dav_objects')) { + await database.customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_dav_objects_href ' + 'ON dav_objects(collection_id, href_key)', + ); await database.customStatement( - 'CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_provider_account ' - 'ON accounts(provider, provider_account_id) ' - 'WHERE provider_account_id IS NOT NULL', + 'CREATE INDEX IF NOT EXISTS idx_dav_objects_projection ' + 'ON dav_objects(collection_id, server_deleted, parser_version, ' + 'last_parse_status)', + ); + } + if (await _hasTable(database, 'dav_object_components')) { + await database.customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_dav_components_logical_key ' + 'ON dav_object_components(dav_object_id, component_type, uid, ' + "IFNULL(recurrence_id_key, ''))", + ); + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_dav_components_uid ' + 'ON dav_object_components(component_type, uid, recurrence_id_key)', ); } if (await _hasTable(database, 'task_lists')) { @@ -199,6 +577,12 @@ Future _createIndexes(AppDatabase database) async { 'CREATE INDEX IF NOT EXISTS idx_task_lists_dirty ' 'ON task_lists(account_id, local_dirty, pending_delete)', ); + if (await _hasColumn(database, 'task_lists', 'dav_collection_id')) { + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_task_lists_dav_collection ' + 'ON task_lists(dav_collection_id)', + ); + } } if (await _hasTable(database, 'tasks')) { await database.customStatement( @@ -217,6 +601,12 @@ Future _createIndexes(AppDatabase database) async { 'CREATE INDEX IF NOT EXISTS idx_tasks_updated ' 'ON tasks(account_id, task_list_id, updated_utc)', ); + if (await _hasColumn(database, 'tasks', 'dav_object_id')) { + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_tasks_dav_component ' + 'ON tasks(dav_object_id, dav_component_id, recurrence_id_key)', + ); + } } if (await _hasTable(database, 'calendar_sources')) { await database.customStatement( @@ -227,6 +617,12 @@ Future _createIndexes(AppDatabase database) async { 'CREATE INDEX IF NOT EXISTS idx_calendar_sources_visible ' 'ON calendar_sources(account_id, selected, hidden, is_deleted)', ); + if (await _hasColumn(database, 'calendar_sources', 'dav_collection_id')) { + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_calendar_sources_dav_collection ' + 'ON calendar_sources(dav_collection_id)', + ); + } } if (await _hasTable(database, 'calendar_events')) { await database.customStatement( @@ -243,12 +639,27 @@ Future _createIndexes(AppDatabase database) async { 'CREATE INDEX IF NOT EXISTS idx_calendar_events_dirty ' 'ON calendar_events(account_id, sync_status, is_deleted)', ); + if (await _hasColumn(database, 'calendar_events', 'dav_object_id')) { + await database.customStatement( + 'CREATE INDEX IF NOT EXISTS idx_calendar_events_dav_occurrence ' + 'ON calendar_events(dav_object_id, dav_component_id, occurrence_key)', + ); + } } - if (await _hasTable(database, 'calendar_sync_states')) { + if (await _hasTable(database, 'sync_cursors')) { + await database.customStatement( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_sync_cursors_scope ' + 'ON sync_cursors(account_id, provider, transport, sync_scope_kind, ' + "IFNULL(dav_collection_id, ''), IFNULL(projection_source_id, ''), " + "cursor_kind, IFNULL(range_start, ''), IFNULL(range_end, ''))", + ); + } + if (await _hasTable(database, 'pending_ops') && + await _hasColumn(database, 'pending_ops', 'dav_collection_id')) { await database.customStatement( - 'CREATE UNIQUE INDEX IF NOT EXISTS idx_calendar_sync_states_scope ' - 'ON calendar_sync_states(account_id, provider, sync_kind, ' - 'calendar_source_id, range_start, range_end)', + 'CREATE INDEX IF NOT EXISTS idx_pending_ops_dav_replay ' + 'ON pending_ops(account_id, dav_collection_id, state, ' + 'next_attempt_at_utc, created_at_utc)', ); } if (await _hasTable(database, 'notification_schedule')) { @@ -260,6 +671,27 @@ Future _createIndexes(AppDatabase database) async { } } +Future _addColumnIfMissing( + Migrator migrator, + AppDatabase database, + TableInfo table, + GeneratedColumn column, +) async { + if (!await _hasTable(database, table.actualTableName)) { + return; + } + if (!await _hasColumn(database, table.actualTableName, column.$name)) { + await migrator.addColumn(table, column); + } +} + +Future _tableCount(AppDatabase database, String tableName) async { + final row = await database + .customSelect('SELECT count(*) AS row_count FROM "$tableName"') + .getSingle(); + return row.read('row_count'); +} + Future _hasTable(AppDatabase database, String tableName) async { final row = await database .customSelect( @@ -269,3 +701,33 @@ Future _hasTable(AppDatabase database, String tableName) async { .getSingleOrNull(); return row != null; } + +Future _hasColumn( + AppDatabase database, + String tableName, + String columnName, +) async { + final rows = await database + .customSelect('PRAGMA table_info("$tableName")') + .get(); + return rows.any((row) => row.read('name') == columnName); +} + +const _legacyCalendarSyncStatesSql = ''' + CREATE TABLE calendar_sync_states ( + id TEXT NOT NULL PRIMARY KEY, + account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + calendar_source_id TEXT NULL REFERENCES calendar_sources(id) + ON DELETE CASCADE, + provider TEXT NOT NULL, + sync_kind TEXT NOT NULL, + range_start TEXT NULL, + range_end TEXT NULL, + google_sync_token TEXT NULL, + microsoft_delta_link TEXT NULL, + last_full_sync_at INTEGER NULL, + last_incremental_sync_at INTEGER NULL, + last_error TEXT NULL, + raw_state_json TEXT NULL + ) +'''; diff --git a/lib/src/db/tables.dart b/lib/src/db/tables.dart index da25a7a..f899a52 100644 --- a/lib/src/db/tables.dart +++ b/lib/src/db/tables.dart @@ -2,8 +2,12 @@ import 'package:drift/drift.dart'; class Accounts extends Table { TextColumn get id => text()(); - TextColumn get provider => text().withDefault(const Constant('google'))(); - TextColumn get providerAccountId => text().nullable()(); + TextColumn get provider => text()(); + TextColumn get authority => text()(); + TextColumn get providerAccountId => text()(); + TextColumn get credentialKind => text()(); + IntColumn get providerProfileVersion => + integer().withDefault(const Constant(1))(); TextColumn get displayName => text().nullable()(); TextColumn get email => text().nullable()(); TextColumn get tenantId => text().nullable()(); @@ -22,12 +26,179 @@ class Accounts extends Table { @override Set> get primaryKey => {id}; + + @override + List get customConstraints => const [ + "CHECK (provider IN ('google', 'microsoft', 'apple_icloud', 'nextcloud'))", + "CHECK (credential_kind IN ('oauth', 'apple_app_specific_password', " + "'nextcloud_app_password'))", + 'CHECK (length(trim(authority)) > 0)', + 'CHECK (length(trim(provider_account_id)) > 0)', + ]; +} + +class DavAccountServices extends Table { + TextColumn get accountId => + text().references(Accounts, #id, onDelete: KeyAction.cascade)(); + TextColumn get canonicalServiceUri => text()(); + TextColumn get canonicalOrigin => text()(); + TextColumn get principalHref => text().nullable()(); + TextColumn get calendarHomeHref => text().nullable()(); + TextColumn get calendarUserAddressesJson => + text().withDefault(const Constant('[]'))(); + TextColumn get scheduleInboxHref => text().nullable()(); + TextColumn get scheduleOutboxHref => text().nullable()(); + TextColumn get capabilitiesJson => text().withDefault(const Constant('{}'))(); + IntColumn get capabilitiesSchemaVersion => + integer().withDefault(const Constant(1))(); + IntColumn get providerProfileVersion => + integer().withDefault(const Constant(1))(); + TextColumn get discoveredAtUtc => text()(); + TextColumn get lastValidatedAtUtc => text().nullable()(); + TextColumn get lastDiscoveryErrorCode => text().nullable()(); + + @override + Set> get primaryKey => {accountId}; +} + +class DavCollections extends Table { + TextColumn get id => text()(); + TextColumn get accountId => + text().references(Accounts, #id, onDelete: KeyAction.cascade)(); + TextColumn get hrefKey => text()(); + TextColumn get requestUri => text()(); + TextColumn get displayName => text()(); + TextColumn get description => text().nullable()(); + TextColumn get resourceTypesJson => + text().withDefault(const Constant('[]'))(); + IntColumn get supportedComponentMask => + integer().withDefault(const Constant(0))(); + TextColumn get supportedCalendarDataJson => + text().withDefault(const Constant('[]'))(); + TextColumn get supportedReportsJson => + text().withDefault(const Constant('[]'))(); + TextColumn get currentUserPrivilegesJson => + text().withDefault(const Constant('[]'))(); + TextColumn get ownerHref => text().nullable()(); + TextColumn get safeDisplayMetadataJson => text().nullable()(); + TextColumn get color => text().nullable()(); + IntColumn get sortOrder => integer().nullable()(); + TextColumn get calendarTimeZone => text().nullable()(); + TextColumn get calendarTimeZoneId => text().nullable()(); + TextColumn get scheduleTransparency => text().nullable()(); + IntColumn get maximumResourceSize => integer().nullable()(); + IntColumn get maximumInstances => integer().nullable()(); + TextColumn get syncToken => text().nullable()(); + TextColumn get ctag => text().nullable()(); + BoolColumn get readOnly => boolean().withDefault(const Constant(true))(); + BoolColumn get eventProjectionEnabled => + boolean().withDefault(const Constant(false))(); + BoolColumn get taskProjectionEnabled => + boolean().withDefault(const Constant(false))(); + BoolColumn get eventsSelected => + boolean().withDefault(const Constant(true))(); + BoolColumn get tasksSelected => boolean().withDefault(const Constant(true))(); + BoolColumn get serverMissing => + boolean().withDefault(const Constant(false))(); + BoolColumn get deleted => boolean().withDefault(const Constant(false))(); + TextColumn get lastInventoryAtUtc => text().nullable()(); + TextColumn get lastSyncAtUtc => text().nullable()(); + IntColumn get parserVersion => integer().withDefault(const Constant(1))(); + IntColumn get projectionVersion => integer().withDefault(const Constant(1))(); + TextColumn get createdAtUtc => text()(); + TextColumn get updatedAtUtc => text()(); + + @override + Set> get primaryKey => {id}; +} + +class DavObjects extends Table { + TextColumn get id => text()(); + TextColumn get accountId => + text().references(Accounts, #id, onDelete: KeyAction.cascade)(); + TextColumn get collectionId => + text().references(DavCollections, #id, onDelete: KeyAction.cascade)(); + TextColumn get hrefKey => text()(); + TextColumn get requestUri => text()(); + TextColumn get etag => text().nullable()(); + TextColumn get contentType => text().nullable()(); + TextColumn get dominantComponentType => text().nullable()(); + IntColumn get componentMask => integer().withDefault(const Constant(0))(); + TextColumn get primaryUid => text().nullable()(); + TextColumn get rawIcsBody => text()(); + TextColumn get rawBodyHash => text()(); + TextColumn get semanticHash => text().nullable()(); + BoolColumn get serverDeleted => + boolean().withDefault(const Constant(false))(); + IntColumn get baselineGeneration => + integer().withDefault(const Constant(0))(); + TextColumn get firstSeenAtUtc => text()(); + TextColumn get lastFetchedAtUtc => text()(); + TextColumn get lastChangedAtUtc => text()(); + TextColumn get lastParseStatus => + text().withDefault(const Constant('unparsed'))(); + TextColumn get lastParseErrorCode => text().nullable()(); + IntColumn get parserVersion => integer().withDefault(const Constant(1))(); + + @override + Set> get primaryKey => {id}; +} + +class DavObjectComponents extends Table { + TextColumn get id => text()(); + TextColumn get davObjectId => + text().references(DavObjects, #id, onDelete: KeyAction.cascade)(); + TextColumn get componentType => text()(); + TextColumn get uid => text()(); + TextColumn get recurrenceIdKey => text().nullable()(); + IntColumn get sequence => integer().nullable()(); + TextColumn get dtstampUtc => text().nullable()(); + TextColumn get lastModifiedUtc => text().nullable()(); + TextColumn get semanticHash => text()(); + IntColumn get parserProfileVersion => + integer().withDefault(const Constant(1))(); + + @override + Set> get primaryKey => {id}; +} + +class DavConflictSnapshots extends Table { + TextColumn get id => text()(); + TextColumn get accountId => + text().references(Accounts, #id, onDelete: KeyAction.cascade)(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davObjectId => text().nullable().references( + DavObjects, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get baselineEtag => text().nullable()(); + TextColumn get baselineRawIcs => text()(); + TextColumn get localCandidateRawIcs => text()(); + TextColumn get remoteEtag => text().nullable()(); + TextColumn get remoteRawIcs => text()(); + TextColumn get conflictCode => text()(); + TextColumn get createdAtUtc => text()(); + TextColumn get resolvedAtUtc => text().nullable()(); + TextColumn get resolution => text().nullable()(); + + @override + Set> get primaryKey => {id}; } class TaskLists extends Table { TextColumn get accountId => text().references(Accounts, #id, onDelete: KeyAction.cascade)(); TextColumn get id => text()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); TextColumn get kind => text().nullable()(); TextColumn get etag => text().nullable()(); TextColumn get title => text()(); @@ -57,6 +228,36 @@ class Tasks extends Table { text().references(Accounts, #id, onDelete: KeyAction.cascade)(); TextColumn get taskListId => text()(); TextColumn get id => text()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davObjectId => text().nullable().references( + DavObjects, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davComponentId => text().nullable().references( + DavObjectComponents, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get icalUid => text().nullable()(); + TextColumn get recurrenceIdKey => text().nullable()(); + IntColumn get icalPriority => integer().nullable()(); + IntColumn get percentComplete => integer().nullable()(); + TextColumn get taskLocation => text().nullable()(); + TextColumn get taskUrl => text().nullable()(); + TextColumn get taskClassification => text().nullable()(); + BoolColumn get taskPinned => boolean().nullable()(); + BoolColumn get taskHideSubtasks => boolean().nullable()(); + BoolColumn get taskHideCompletedSubtasks => boolean().nullable()(); + TextColumn get taskAlarmsJson => text().nullable()(); + TextColumn get parentUid => text().nullable()(); + IntColumn get sortOrder => integer().nullable()(); + TextColumn get providerExtensionProjectionJson => text().nullable()(); + IntColumn get projectionVersion => integer().withDefault(const Constant(1))(); TextColumn get kind => text().nullable()(); TextColumn get etag => text().nullable()(); TextColumn get title => text()(); @@ -80,6 +281,7 @@ class Tasks extends Table { BoolColumn get microsoftIsReminderOn => boolean().nullable()(); TextColumn get microsoftCompletedDateTime => text().nullable()(); TextColumn get microsoftCompletedTimeZone => text().nullable()(); + TextColumn get microsoftChecklistItemsJson => text().nullable()(); TextColumn get recurrenceJson => text().nullable()(); TextColumn get importance => text().nullable()(); TextColumn get categoriesJson => text().nullable()(); @@ -126,6 +328,38 @@ class PendingOps extends Table { TextColumn get calendarSourceId => text().nullable()(); TextColumn get providerCalendarId => text().nullable()(); TextColumn get eventId => text().nullable()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davCollectionHref => text().nullable()(); + TextColumn get davObjectId => text().nullable().references( + DavObjects, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davMemberHref => text().nullable()(); + TextColumn get baselineEtag => text().nullable()(); + TextColumn get baselineRawIcs => text().nullable()(); + TextColumn get mutationPatchJson => text().nullable()(); + IntColumn get mutationPatchSchemaVersion => integer().nullable()(); + TextColumn get targetComponentKey => text().nullable()(); + TextColumn get mutationScope => text().nullable()(); + TextColumn get destinationCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get destinationCollectionHref => text().nullable()(); + TextColumn get destinationMemberHref => text().nullable()(); + TextColumn get conflictState => text().nullable()(); + TextColumn get conflictSnapshotId => text().nullable().references( + DavConflictSnapshots, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get retryClassification => text().nullable()(); TextColumn get localTempId => text().nullable()(); TextColumn get dependsOnOpId => text().nullable()(); TextColumn get requestJson => text()(); @@ -150,6 +384,11 @@ class CalendarSources extends Table { text().references(Accounts, #id, onDelete: KeyAction.cascade)(); TextColumn get provider => text()(); TextColumn get providerCalendarId => text()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); TextColumn get summary => text()(); TextColumn get description => text().nullable()(); BoolColumn get primaryCalendar => @@ -180,6 +419,25 @@ class CalendarEvents extends Table { TextColumn get provider => text()(); TextColumn get providerCalendarId => text()(); TextColumn get providerEventId => text()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davObjectId => text().nullable().references( + DavObjects, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get davComponentId => text().nullable().references( + DavObjectComponents, + #id, + onDelete: KeyAction.setNull, + )(); + TextColumn get icalUid => text().nullable()(); + TextColumn get recurrenceIdKey => text().nullable()(); + TextColumn get occurrenceKey => text().nullable()(); + IntColumn get projectionVersion => integer().withDefault(const Constant(1))(); TextColumn get providerRecurringEventId => text().nullable()(); TextColumn get providerOriginalStartKey => text().nullable()(); TextColumn get etagOrChangeKey => text().nullable()(); @@ -253,25 +511,36 @@ class CalendarEventReminders extends Table { Set> get primaryKey => {id}; } -class CalendarSyncStates extends Table { +class SyncCursors extends Table { TextColumn get id => text()(); TextColumn get accountId => text().references(Accounts, #id, onDelete: KeyAction.cascade)(); - TextColumn get calendarSourceId => text().nullable().references( + TextColumn get projectionSourceId => text().nullable().references( CalendarSources, #id, onDelete: KeyAction.cascade, )(); TextColumn get provider => text()(); - TextColumn get syncKind => text()(); + TextColumn get transport => text()(); + TextColumn get syncScopeKind => text()(); + TextColumn get davCollectionId => text().nullable().references( + DavCollections, + #id, + onDelete: KeyAction.cascade, + )(); + TextColumn get cursorKind => text()(); + TextColumn get cursorValue => text()(); TextColumn get rangeStart => text().nullable()(); TextColumn get rangeEnd => text().nullable()(); - TextColumn get googleSyncToken => text().nullable()(); - TextColumn get microsoftDeltaLink => text().nullable()(); - IntColumn get lastFullSyncAt => integer().nullable()(); - IntColumn get lastIncrementalSyncAt => integer().nullable()(); - TextColumn get lastError => text().nullable()(); - TextColumn get rawStateJson => text().nullable()(); + IntColumn get baselineGeneration => + integer().withDefault(const Constant(0))(); + TextColumn get inProgressCursor => text().nullable()(); + IntColumn get inProgressGeneration => integer().nullable()(); + IntColumn get lastCompleteSyncAt => integer().nullable()(); + TextColumn get lastFailureCode => text().nullable()(); + IntColumn get stateSchemaVersion => + integer().withDefault(const Constant(1))(); + TextColumn get stateJson => text().nullable()(); @override Set> get primaryKey => {id}; diff --git a/lib/src/demo/demo_profile.dart b/lib/src/demo/demo_profile.dart index 92c9194..a5b1935 100644 --- a/lib/src/demo/demo_profile.dart +++ b/lib/src/demo/demo_profile.dart @@ -14,9 +14,9 @@ import '../features/notifications/notification_scheduler.dart'; import '../features/sync/account_sync_operations.dart'; import '../features/sync/all_accounts_sync_scheduler.dart'; import '../google_tasks/api/google_tasks_api_surface.dart'; -import '../google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import '../google_tasks/oauth/oauth_service.dart'; -import '../google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import 'demo_seed.dart'; AppSettings busyMaxDemoSettings(BusyMaxDemoTheme theme) { @@ -85,7 +85,7 @@ class BusyMaxDemoProfile { ref.onDispose(client.close); return client; }), - oAuthTokenStoreProvider.overrideWithValue(InMemoryOAuthTokenStore()), + secretStoreProvider.overrideWithValue(InMemorySecretStore()), applicationOAuthGatewayProvider.overrideWithValue( DemoOAuthGateway(activeAccountId: busyMaxDemoAccountId), ), diff --git a/lib/src/demo/demo_seed.dart b/lib/src/demo/demo_seed.dart index 23c60ac..7ca041a 100644 --- a/lib/src/demo/demo_seed.dart +++ b/lib/src/demo/demo_seed.dart @@ -24,8 +24,10 @@ Future seedBusyMaxDemoData(AppDatabase database, {DateTime? now}) async { .insert( AccountsCompanion.insert( id: busyMaxDemoAccountId, - provider: const Value('google'), - providerAccountId: const Value('demo-user'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'demo-user', + credentialKind: 'oauth', displayName: const Value('Alex Morgan'), email: const Value('alex@example.com'), authState: const Value(accountAuthStateSignedIn), diff --git a/lib/src/features/accounts/data/accounts_repository.dart b/lib/src/features/accounts/data/accounts_repository.dart index eadfa6b..3f125db 100644 --- a/lib/src/features/accounts/data/accounts_repository.dart +++ b/lib/src/features/accounts/data/accounts_repository.dart @@ -3,53 +3,90 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import '../../../db/app_database.dart'; -import '../../../task_providers/task_provider.dart'; +import '../domain/account_connection_state.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/providers/account_authority.dart'; const accountAuthStateSignedIn = 'signed_in'; const accountAuthStateReauthRequired = 'reauth_required'; +const accountAuthStateTemporarilyUnavailable = 'temporarily_unavailable'; +const accountAuthStatePermissionChanged = 'permission_changed'; +const accountAuthStateUnsupportedServer = 'unsupported_server_profile'; + +const accountCachedAvailableStates = [ + accountAuthStateSignedIn, + accountAuthStateReauthRequired, + accountAuthStateTemporarilyUnavailable, + accountAuthStatePermissionChanged, + accountAuthStateUnsupportedServer, +]; class AccountEntity { const AccountEntity({ required this.id, required this.provider, + required this.authority, + required this.providerAccountId, + this.credentialKind = CredentialKind.oauth, + this.providerProfileVersion = 1, required this.authState, - this.providerAccountId, this.displayName, this.email, this.tenantId, this.providerMetadataJson, this.calendarsEnabled = true, this.tasksEnabled = true, + this.lastSuccessfulSyncAtUtc, + this.lastFullSyncAtUtc, }); factory AccountEntity.fromRow(Account row) { return AccountEntity( id: row.id, - provider: TaskProviderParsing.fromStorageValue(row.provider), + provider: BusyProviderCodec.requireStorageValue(row.provider), + authority: row.authority, providerAccountId: row.providerAccountId, + credentialKind: _credentialKindFromStorage(row.credentialKind), + providerProfileVersion: row.providerProfileVersion, displayName: row.displayName, email: row.email, tenantId: row.tenantId, providerMetadataJson: row.providerMetadataJson, calendarsEnabled: row.calendarsEnabled, tasksEnabled: row.tasksEnabled, + lastSuccessfulSyncAtUtc: DateTime.tryParse( + row.lastSuccessfulSyncAtUtc ?? '', + )?.toUtc(), + lastFullSyncAtUtc: DateTime.tryParse( + row.lastFullSyncAtUtc ?? '', + )?.toUtc(), authState: row.authState, ); } final String id; - final TaskProvider provider; - final String? providerAccountId; + final BusyProvider provider; + final String authority; + final String providerAccountId; + final CredentialKind credentialKind; + final int providerProfileVersion; final String? displayName; final String? email; final String? tenantId; final String? providerMetadataJson; final bool calendarsEnabled; final bool tasksEnabled; + final DateTime? lastSuccessfulSyncAtUtc; + final DateTime? lastFullSyncAtUtc; final String authState; + AccountConnectionState get connectionState => + AccountConnectionStateCodec.parse(authState); + bool get isSignedIn => authState == accountAuthStateSignedIn; bool get needsReconnect => authState == accountAuthStateReauthRequired; + bool get hasConnectionIssue => authState != accountAuthStateSignedIn; String get displayLabel { final name = displayName?.trim(); @@ -70,6 +107,36 @@ class AccountEntity { } return address; } + + String get selectorLabel { + final providerLabel = provider.displayName; + final identity = _selectorIdentity; + if (identity == null || + identity.toLowerCase() == providerLabel.toLowerCase()) { + return providerLabel; + } + return '$providerLabel · $identity'; + } + + String? get _selectorIdentity { + final address = _trimmedAccountValue(email); + if (address != null) return address; + final name = _trimmedAccountValue(displayName); + if (provider != BusyProvider.nextcloud) return name; + final uri = Uri.tryParse(authority); + final host = uri == null || uri.host.isEmpty + ? null + : uri.hasPort + ? '${uri.host}:${uri.port}' + : uri.host; + if (name == null) return host; + return host == null ? name : '$name · $host'; + } +} + +String? _trimmedAccountValue(String? value) { + final trimmed = value?.trim(); + return trimmed == null || trimmed.isEmpty ? null : trimmed; } class AccountsRepository { @@ -84,7 +151,7 @@ class AccountsRepository { Stream> watchAccounts() { final query = _database.select(_database.accounts) - ..where((row) => row.authState.equals(accountAuthStateSignedIn)) + ..where((row) => row.authState.isIn(accountCachedAvailableStates)) ..orderBy([ (row) => OrderingTerm.asc(row.provider), (row) => OrderingTerm.asc(row.displayName), @@ -97,12 +164,7 @@ class AccountsRepository { Stream> watchVisibleAccounts() { final query = _database.select(_database.accounts) - ..where( - (row) => row.authState.isIn([ - accountAuthStateSignedIn, - accountAuthStateReauthRequired, - ]), - ) + ..where((row) => row.authState.isIn([...accountCachedAvailableStates])) ..orderBy([ (row) => OrderingTerm.asc(row.provider), (row) => OrderingTerm.asc(row.displayName), @@ -125,6 +187,33 @@ class AccountsRepository { return rows.map(AccountEntity.fromRow).toList(); } + Future> listSyncEligibleAccounts() async { + final query = _database.select(_database.accounts) + ..where( + (row) => row.authState.isIn(const [ + accountAuthStateSignedIn, + accountAuthStateTemporarilyUnavailable, + ]), + ) + ..orderBy([ + (row) => OrderingTerm.asc(row.provider), + (row) => OrderingTerm.asc(row.displayName), + (row) => OrderingTerm.asc(row.email), + ]); + return (await query.get()).map(AccountEntity.fromRow).toList(); + } + + Future> listVisibleAccounts() async { + final query = _database.select(_database.accounts) + ..where((row) => row.authState.isIn(accountCachedAvailableStates)) + ..orderBy([ + (row) => OrderingTerm.asc(row.provider), + (row) => OrderingTerm.asc(row.displayName), + (row) => OrderingTerm.asc(row.email), + ]); + return (await query.get()).map(AccountEntity.fromRow).toList(); + } + Future accountById(String accountId) async { final row = await (_database.select( _database.accounts, @@ -134,9 +223,12 @@ class AccountsRepository { Future upsertSignedInAccount({ required String id, - required TaskProvider provider, + required BusyProvider provider, required String grantedScopes, String? providerAccountId, + String? authority, + CredentialKind? credentialKind, + int providerProfileVersion = 1, String? displayName, String? email, String? tenantId, @@ -145,12 +237,26 @@ class AccountsRepository { Map? providerMetadata, }) async { final now = _now(); + final normalizedAuthority = normalizeAccountAuthority( + provider, + authority: authority, + tenantId: tenantId, + ); + final normalizedProviderAccountId = normalizeProviderAccountId( + provider, + providerAccountId ?? id, + ); + final resolvedCredentialKind = + credentialKind ?? _defaultCredentialKind(provider); final existing = await (_database.select( _database.accounts, )..where((account) => account.id.equals(id))).getSingleOrNull(); final companion = AccountsCompanion( provider: Value(provider.storageValue), - providerAccountId: Value(providerAccountId), + authority: Value(normalizedAuthority), + providerAccountId: Value(normalizedProviderAccountId), + credentialKind: Value(resolvedCredentialKind.storageValue), + providerProfileVersion: Value(providerProfileVersion), displayName: Value(displayName), email: Value(email), tenantId: Value(tenantId), @@ -172,8 +278,11 @@ class AccountsRepository { id: id, createdAtUtc: now, updatedAtUtc: now, - provider: Value(provider.storageValue), - providerAccountId: Value(providerAccountId), + provider: provider.storageValue, + authority: normalizedAuthority, + providerAccountId: normalizedProviderAccountId, + credentialKind: resolvedCredentialKind.storageValue, + providerProfileVersion: Value(providerProfileVersion), displayName: Value(displayName), email: Value(email), tenantId: Value(tenantId), @@ -195,11 +304,21 @@ class AccountsRepository { } Future markReconnectRequired(String accountId) { + return setConnectionState( + accountId, + AccountConnectionState.reauthenticationRequired, + ); + } + + Future setConnectionState( + String accountId, + AccountConnectionState state, + ) { return (_database.update( _database.accounts, )..where((account) => account.id.equals(accountId))).write( AccountsCompanion( - authState: const Value(accountAuthStateReauthRequired), + authState: Value(state.storageValue), updatedAtUtc: Value(_now()), ), ); @@ -213,3 +332,19 @@ class AccountsRepository { String _now() => _nowUtc().toIso8601String(); } + +CredentialKind _defaultCredentialKind(BusyProvider provider) => + switch (provider) { + BusyProvider.google || BusyProvider.microsoft => CredentialKind.oauth, + BusyProvider.appleICloud => CredentialKind.appleAppSpecificPassword, + BusyProvider.nextcloud => CredentialKind.nextcloudAppPassword, + }; + +CredentialKind _credentialKindFromStorage(String value) => switch (value) { + 'oauth' => CredentialKind.oauth, + 'apple_app_specific_password' => CredentialKind.appleAppSpecificPassword, + 'nextcloud_app_password' => CredentialKind.nextcloudAppPassword, + _ => throw SecretStoreCorruptException( + 'Unsupported stored credential kind for account metadata.', + ), +}; diff --git a/lib/src/features/accounts/domain/account_connection_state.dart b/lib/src/features/accounts/domain/account_connection_state.dart new file mode 100644 index 0000000..14e639d --- /dev/null +++ b/lib/src/features/accounts/domain/account_connection_state.dart @@ -0,0 +1,39 @@ +enum AccountConnectionState { + connected, + connecting, + reauthenticationRequired, + temporarilyUnavailable, + permissionChanged, + unsupportedServerProfile, + signedOut, +} + +extension AccountConnectionStateStorage on AccountConnectionState { + /// `signed_in` and `reauth_required` are retained as the durable spellings + /// used by the pre-DAV schema. The typed model exposes the provider-neutral + /// connection states required by every transport. + String get storageValue => switch (this) { + AccountConnectionState.connected => 'signed_in', + AccountConnectionState.connecting => 'connecting', + AccountConnectionState.reauthenticationRequired => 'reauth_required', + AccountConnectionState.temporarilyUnavailable => 'temporarily_unavailable', + AccountConnectionState.permissionChanged => 'permission_changed', + AccountConnectionState.unsupportedServerProfile => + 'unsupported_server_profile', + AccountConnectionState.signedOut => 'signed_out', + }; +} + +abstract final class AccountConnectionStateCodec { + static AccountConnectionState parse(String value) => switch (value) { + 'signed_in' => AccountConnectionState.connected, + 'connecting' => AccountConnectionState.connecting, + 'reauth_required' => AccountConnectionState.reauthenticationRequired, + 'temporarily_unavailable' => AccountConnectionState.temporarilyUnavailable, + 'permission_changed' => AccountConnectionState.permissionChanged, + 'unsupported_server_profile' => + AccountConnectionState.unsupportedServerProfile, + 'signed_out' => AccountConnectionState.signedOut, + _ => throw FormatException('Unsupported account connection state.'), + }; +} diff --git a/lib/src/features/auth/data/auth_repository.dart b/lib/src/features/auth/data/auth_repository.dart index 9f63a29..a7bc0e0 100644 --- a/lib/src/features/auth/data/auth_repository.dart +++ b/lib/src/features/auth/data/auth_repository.dart @@ -6,16 +6,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import '../../../core/logging/redacting_logger.dart'; +import '../../../dav/dav_errors.dart'; import '../../../db/app_database.dart'; import '../../../features/accounts/data/accounts_repository.dart'; import '../../../google_tasks/api/google_tasks_api_surface.dart'; import '../../../google_tasks/oauth/oauth_loopback_flow.dart'; -import '../../../google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import '../../../google_tasks/oauth/oauth_service.dart'; -import '../../../google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import '../../../microsoft_todo/oauth/microsoft_oauth_service.dart'; import '../../sync/sync_auth_error.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; enum AuthSessionStatus { unconfigured, @@ -105,7 +106,11 @@ class AuthRepository { final RedactingLogger _logger = RedactingLogger(Logger('AuthRepository')); Future loadSession() async { - final accounts = await _accountsRepository.listSignedInAccounts(); + final connectedAccounts = await _accountsRepository.listSignedInAccounts(); + if (connectedAccounts.isNotEmpty) { + return AuthSessionState.signedIn(connectedAccounts.first.id); + } + final accounts = await _accountsRepository.listVisibleAccounts(); if (accounts.isEmpty) { return const AuthSessionState.signedOut(); } @@ -153,7 +158,7 @@ class AuthRepository { await _accountsRepository.upsertSignedInAccount( id: result.accountId, - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerAccountId: result.user.id, displayName: result.user.displayName, email: result.user.mail ?? result.user.userPrincipalName, @@ -187,10 +192,15 @@ class AuthRepository { await _deleteScheduledNotifications(accountId); }); switch (account.provider) { - case TaskProvider.microsoft: + case BusyProvider.microsoft: await _microsoftOAuth?.signOutAccount(accountId); - case TaskProvider.google: + case BusyProvider.google: await _oAuth.clearLocalSession(accountId: accountId); + case BusyProvider.appleICloud: + case BusyProvider.nextcloud: + // DAV credentials are cleared through SecretStore once the account is + // moved to reauthentication-required state. + break; } } @@ -204,7 +214,7 @@ class AuthRepository { } var revocationStatus = AccountAuthorizationRevocationStatus.notRequested; - if (revokeAuthorization && account.provider == TaskProvider.google) { + if (revokeAuthorization && account.provider == BusyProvider.google) { try { await _oAuth.revokeAuthorization(accountId); revocationStatus = AccountAuthorizationRevocationStatus.succeeded; @@ -217,7 +227,7 @@ class AuthRepository { } } - if (account.provider == TaskProvider.microsoft) { + if (account.provider == BusyProvider.microsoft) { await _microsoftOAuth?.signOutAccount(accountId); } else { await _oAuth.clearLocalSession(accountId: accountId); @@ -291,7 +301,7 @@ class AuthRepository { final userInfo = await _fetchGoogleUserInfo(tokenSet); await _accountsRepository.upsertSignedInAccount( id: accountId, - provider: TaskProvider.google, + provider: BusyProvider.google, providerAccountId: _firstNonBlank([ userInfo?.subject, idTokenClaims['sub']?.toString(), @@ -474,6 +484,9 @@ class AuthSessionController extends StateNotifier { } String authErrorMessage(Object error) { + if (error is DavException) { + return error.safeMessage; + } if (error is OAuthException) { if (_isCallbackFailure(error.code)) { if (error.message == microsoftSignInCallbackNotReceivedMessage) { @@ -484,7 +497,7 @@ String authErrorMessage(Object error) { return error.message; } if (error is PlatformException) { - return secureTokenStorageUnavailableMessage; + return secretStorageUnavailableMessage; } return error.toString(); } diff --git a/lib/src/features/auth/presentation/sign_in_screen.dart b/lib/src/features/auth/presentation/sign_in_screen.dart index ec113c5..d9d45eb 100644 --- a/lib/src/features/auth/presentation/sign_in_screen.dart +++ b/lib/src/features/auth/presentation/sign_in_screen.dart @@ -14,10 +14,14 @@ import '../../../app/busymax_design.dart'; import '../../../app/busymax_glyphs.dart'; import '../../../app/busymax_keyboard_shortcuts_dialog.dart'; import '../../../app/busymax_yaru_theme.dart'; +import '../../../dav/auth/dav_account_dialogs.dart'; +import '../../../dav/dav_errors.dart'; +import '../../../dav/http/dav_http_transport.dart'; import '../../accounts/data/accounts_repository.dart'; +import '../../accounts/domain/account_connection_state.dart'; import '../../../google_tasks/oauth/oauth_loopback_flow.dart'; -import '../../../google_tasks/oauth/oauth_models.dart'; -import '../../../google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import '../../../l10n/l10n.dart'; import '../../../microsoft_todo/oauth/microsoft_oauth_service.dart'; import '../../../platform/linux_header_bar_service.dart'; @@ -25,7 +29,7 @@ import '../../sync/sync_auth_error.dart'; enum _OnboardingStep { accounts, preferences } -enum _OnboardingProvider { google, microsoft } +enum _OnboardingProvider { google, microsoft, appleICloud, nextcloud } class SignInScreen extends ConsumerStatefulWidget { const SignInScreen({super.key}); @@ -41,6 +45,7 @@ class _SignInScreenState extends ConsumerState { var _headerBarReady = false; var _nativeHeaderBarAvailable = false; var _finishingSetup = false; + DavCancellationToken? _davCancellation; var _headerBarUpdateGeneration = 0; late final LinuxHeaderBarSession _headerBarSession; StreamSubscription? _headerBarActions; @@ -154,6 +159,12 @@ class _SignInScreenState extends ConsumerState { isMicrosoftSigningIn: _signingInProvider == _OnboardingProvider.microsoft, + isAppleSigningIn: + _signingInProvider == + _OnboardingProvider.appleICloud, + isNextcloudSigningIn: + _signingInProvider == + _OnboardingProvider.nextcloud, errorMessage: _errorMessage, missingConfigMessage: kReleaseMode ? l10n.providerNotConfigured @@ -162,6 +173,11 @@ class _SignInScreenState extends ConsumerState { _signIn(_OnboardingProvider.google), onAddMicrosoft: () => _signIn(_OnboardingProvider.microsoft), + onAddApple: () => _signIn( + _OnboardingProvider.appleICloud, + ), + onAddNextcloud: () => + _signIn(_OnboardingProvider.nextcloud), onCancelSignIn: _cancelSignIn, ), _OnboardingStep.preferences => @@ -298,6 +314,21 @@ class _SignInScreenState extends ConsumerState { if (_signingInProvider != null) { return; } + AppleICloudCredentialInput? appleInput; + String? nextcloudServer; + if (provider == _OnboardingProvider.appleICloud) { + appleInput = await showAppleICloudCredentialDialog( + context, + headerBarService: ref.read(linuxHeaderBarServiceProvider), + ); + if (appleInput == null || !mounted) return; + } else if (provider == _OnboardingProvider.nextcloud) { + nextcloudServer = await showNextcloudServerDialog( + context, + headerBarService: ref.read(linuxHeaderBarServiceProvider), + ); + if (nextcloudServer == null || !mounted) return; + } setState(() { _signingInProvider = provider; _errorMessage = null; @@ -305,11 +336,36 @@ class _SignInScreenState extends ConsumerState { try { final repository = ref.read(authRepositoryProvider); - final signedIn = switch (provider) { - _OnboardingProvider.google => await repository.signIn(), - _OnboardingProvider.microsoft => await repository.signInWithMicrosoft(), - }; - final accountId = signedIn.accountId; + String? accountId; + switch (provider) { + case _OnboardingProvider.google: + accountId = (await repository.signIn()).accountId; + case _OnboardingProvider.microsoft: + accountId = (await repository.signInWithMicrosoft()).accountId; + case _OnboardingProvider.appleICloud: + final cancellation = DavCancellationToken(); + _davCancellation = cancellation; + accountId = + (await ref + .read(davAccountOnboardingServiceProvider) + .connectAppleICloud( + email: appleInput!.email, + appSpecificPassword: appleInput.password, + cancellationToken: cancellation, + )) + .accountId; + case _OnboardingProvider.nextcloud: + final cancellation = DavCancellationToken(); + _davCancellation = cancellation; + accountId = + (await ref + .read(davAccountOnboardingServiceProvider) + .connectNextcloud( + enteredServer: nextcloudServer!, + cancellationToken: cancellation, + )) + .accountId; + } if (accountId != null) { unawaited(_runInitialSync(accountId)); } @@ -322,7 +378,10 @@ class _SignInScreenState extends ConsumerState { } } finally { if (mounted) { - setState(() => _signingInProvider = null); + setState(() { + _signingInProvider = null; + _davCancellation = null; + }); } } } @@ -347,6 +406,8 @@ class _SignInScreenState extends ConsumerState { } Future _cancelSignIn() async { + _davCancellation?.cancel(); + ref.read(davAccountOnboardingServiceProvider).cancelNextcloudLogin(); await ref.read(authRepositoryProvider).cancelSignIn(); if (mounted) { setState(() => _signingInProvider = null); @@ -404,10 +465,14 @@ class _AccountsOnboardingStep extends StatelessWidget { required this.microsoftConfigured, required this.isGoogleSigningIn, required this.isMicrosoftSigningIn, + required this.isAppleSigningIn, + required this.isNextcloudSigningIn, required this.errorMessage, required this.missingConfigMessage, required this.onAddGoogle, required this.onAddMicrosoft, + required this.onAddApple, + required this.onAddNextcloud, required this.onCancelSignIn, }); @@ -416,32 +481,33 @@ class _AccountsOnboardingStep extends StatelessWidget { final bool microsoftConfigured; final bool isGoogleSigningIn; final bool isMicrosoftSigningIn; + final bool isAppleSigningIn; + final bool isNextcloudSigningIn; final String? errorMessage; final String missingConfigMessage; final VoidCallback onAddGoogle; final VoidCallback onAddMicrosoft; + final VoidCallback onAddApple; + final VoidCallback onAddNextcloud; final VoidCallback onCancelSignIn; @override Widget build(BuildContext context) { final l10n = context.l10n; final colorScheme = Theme.of(context).colorScheme; - final isSigningIn = isGoogleSigningIn || isMicrosoftSigningIn; + final isSigningIn = + isGoogleSigningIn || + isMicrosoftSigningIn || + isAppleSigningIn || + isNextcloudSigningIn; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _OnboardingStepHeader( title: l10n.onboardingAccountsStepTitle, - description: l10n.connectGoogleAccount, + description: l10n.providerConnectionDescription, ), const SizedBox(height: BusyMaxSpacing.xl), - Text( - l10n.googlePermissionsConsentNotice, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant), - ), - const SizedBox(height: BusyMaxSpacing.md), _ProviderSignInButton( label: l10n.addGoogleAccount, loadingLabel: l10n.waitingForGoogleSignIn, @@ -453,6 +519,13 @@ class _AccountsOnboardingStep extends StatelessWidget { : l10n.providerNotConfigured, onPressed: onAddGoogle, ), + const SizedBox(height: BusyMaxSpacing.sm), + Text( + l10n.googlePermissionsConsentNotice, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant), + ), const SizedBox(height: BusyMaxSpacing.md), _ProviderSignInButton( label: l10n.addMicrosoftAccount, @@ -465,6 +538,26 @@ class _AccountsOnboardingStep extends StatelessWidget { : l10n.providerNotConfigured, onPressed: onAddMicrosoft, ), + const SizedBox(height: BusyMaxSpacing.md), + _ProviderSignInButton( + label: l10n.addAppleICloudAccount, + loadingLabel: l10n.waitingForAppleICloud, + configured: true, + enabled: !isSigningIn, + loading: isAppleSigningIn, + tooltip: l10n.addAppleICloudAccount, + onPressed: onAddApple, + ), + const SizedBox(height: BusyMaxSpacing.md), + _ProviderSignInButton( + label: l10n.addNextcloudAccount, + loadingLabel: l10n.waitingForNextcloud, + configured: true, + enabled: !isSigningIn, + loading: isNextcloudSigningIn, + tooltip: l10n.addNextcloudAccount, + onPressed: onAddNextcloud, + ), if (accounts.isNotEmpty) ...[ const SizedBox(height: BusyMaxSpacing.lg), _SignedInAccountsSummary(accounts: accounts), @@ -534,15 +627,15 @@ class _SignedInAccountRow extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final secondary = account.secondaryLabel; - final needsReconnect = account.needsReconnect; + final hasIssue = account.hasConnectionIssue; return Padding( padding: const EdgeInsets.symmetric(vertical: BusyMaxSpacing.xs), child: Row( children: [ Icon( - needsReconnect ? YaruIcons.warning : YaruIcons.checkmark, + hasIssue ? YaruIcons.warning : YaruIcons.checkmark, size: BusyMaxSizes.iconSm, - color: needsReconnect ? colorScheme.error : colorScheme.primary, + color: hasIssue ? colorScheme.error : colorScheme.primary, ), const SizedBox(width: BusyMaxSpacing.sm), Expanded( @@ -554,9 +647,9 @@ class _SignedInAccountRow extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), - if (needsReconnect) + if (hasIssue) Text( - accountReconnectRequiredSyncMessage, + _onboardingAccountIssueMessage(context, account), maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of( @@ -581,6 +674,22 @@ class _SignedInAccountRow extends StatelessWidget { } } +String _onboardingAccountIssueMessage( + BuildContext context, + AccountEntity account, +) => switch (account.connectionState) { + AccountConnectionState.reauthenticationRequired => + context.l10n.davReauthenticationRequired, + AccountConnectionState.temporarilyUnavailable => + context.l10n.davTemporarilyUnavailable, + AccountConnectionState.permissionChanged => context.l10n.davPermissionChanged, + AccountConnectionState.unsupportedServerProfile => + context.l10n.davUnsupportedServer, + AccountConnectionState.connected || + AccountConnectionState.connecting || + AccountConnectionState.signedOut => '', +}; + class _PreferencesOnboardingStep extends StatelessWidget { const _PreferencesOnboardingStep({ required this.settings, @@ -811,6 +920,9 @@ ButtonStyle _onboardingTextButtonStyle(BuildContext context) { } String _onboardingErrorMessage(BuildContext context, Object error) { + if (error is DavException) return error.safeMessage; + if (error is FormatException) return error.message; + if (error is SecretStoreException) return error.message; if (error is OAuthException) { if (error.code == 'OAuthMissingRequiredScope') { return context.l10n.googlePermissionsRequiredRetry; @@ -824,7 +936,7 @@ String _onboardingErrorMessage(BuildContext context, Object error) { return error.message; } if (error is PlatformException) { - return secureTokenStorageUnavailableMessage; + return secretStorageUnavailableMessage; } return error.toString(); } diff --git a/lib/src/features/calendar/data/calendar_repository.dart b/lib/src/features/calendar/data/calendar_repository.dart index dc8509f..c32f107 100644 --- a/lib/src/features/calendar/data/calendar_repository.dart +++ b/lib/src/features/calendar/data/calendar_repository.dart @@ -5,9 +5,15 @@ import 'package:uuid/uuid.dart'; import '../../../calendar_providers/calendar_mutation.dart'; import '../../../calendar_providers/calendar_sync_dto.dart'; +import '../../../dav/ical/ical_document.dart'; +import '../../../dav/ical/ical_semantics.dart'; +import '../../../dav/mutation/dav_mutation_patch.dart'; +import '../../../dav/mutation/dav_pending_operations.dart'; +import '../../../dav/mutation/dav_projection_mutations.dart'; +import '../../../dav/storage/dav_object_repository.dart'; import '../../../db/app_database.dart'; import '../../notifications/notification_schedule_service.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../presentation/event_editor_draft.dart'; class CalendarSourceEntity { @@ -21,31 +27,35 @@ class CalendarSourceEntity { required this.hidden, required this.readOnly, required this.isDeleted, + this.primaryCalendar = false, this.description, this.backgroundColor, this.foregroundColor, this.colorId, this.timeZone, this.accessRole, + this.davCollectionId, }); factory CalendarSourceEntity.fromRow(CalendarSource row) { return CalendarSourceEntity( id: row.id, accountId: row.accountId, - provider: TaskProviderParsing.fromStorageValue(row.provider), + provider: BusyProviderCodec.requireStorageValue(row.provider), providerCalendarId: row.providerCalendarId, summary: row.summary, selected: row.selected, hidden: row.hidden, readOnly: row.readOnly, isDeleted: row.isDeleted, + primaryCalendar: row.primaryCalendar, description: row.description, backgroundColor: row.backgroundColor, foregroundColor: row.foregroundColor, colorId: row.colorId, timeZone: row.timeZone, accessRole: row.accessRole, + davCollectionId: row.davCollectionId, ); } @@ -58,12 +68,14 @@ class CalendarSourceEntity { final bool hidden; final bool readOnly; final bool isDeleted; + final bool primaryCalendar; final String? description; final String? backgroundColor; final String? foregroundColor; final String? colorId; final String? timeZone; final String? accessRole; + final String? davCollectionId; CalendarSourceCapabilities get capabilities => CalendarSourceCapabilities.fromSource(this); @@ -439,11 +451,11 @@ class CalendarRepository { String? calendarSourceId, String? rangeStart, String? rangeEnd, - String? googleSyncToken, - String? microsoftDeltaLink, + required String cursorKind, + required String cursorValue, bool full = false, String? lastError, - String? rawStateJson, + String? stateJson, }) async { final id = syncStateId( accountId: accountId, @@ -456,34 +468,35 @@ class CalendarRepository { // Older releases keyed state by exact range bounds. Remove those rows // before inserting the stable scope key; the legacy unique index still // includes the range columns. - await (_database.delete(_database.calendarSyncStates)..where((row) { + await (_database.delete(_database.syncCursors)..where((row) { final sameSource = calendarSourceId == null - ? row.calendarSourceId.isNull() - : row.calendarSourceId.equals(calendarSourceId); + ? row.projectionSourceId.isNull() + : row.projectionSourceId.equals(calendarSourceId); return row.accountId.equals(accountId) & row.provider.equals(provider.storageValue) & - row.syncKind.equals(syncKind) & + row.syncScopeKind.equals(syncKind) & sameSource & row.id.equals(id).not(); })) .go(); await _database - .into(_database.calendarSyncStates) + .into(_database.syncCursors) .insertOnConflictUpdate( - CalendarSyncStatesCompanion.insert( + SyncCursorsCompanion.insert( id: id, accountId: accountId, - calendarSourceId: Value(calendarSourceId), + projectionSourceId: Value(calendarSourceId), provider: provider.storageValue, - syncKind: syncKind, + transport: 'rest', + syncScopeKind: syncKind, + cursorKind: cursorKind, + cursorValue: cursorValue, rangeStart: Value(rangeStart), rangeEnd: Value(rangeEnd), - googleSyncToken: Value(googleSyncToken), - microsoftDeltaLink: Value(microsoftDeltaLink), - lastFullSyncAt: full ? Value(now) : const Value.absent(), - lastIncrementalSyncAt: full ? const Value.absent() : Value(now), - lastError: Value(lastError), - rawStateJson: Value(rawStateJson), + baselineGeneration: Value(full ? now : 0), + lastCompleteSyncAt: Value(now), + lastFailureCode: Value(lastError), + stateJson: Value(stateJson), ), ); }); @@ -504,8 +517,11 @@ class CalendarRepository { sourceId: source.id, ); } + if (source.davCollectionId != null) { + return _createLocalDavEvent(source, draft); + } final now = _now().millisecondsSinceEpoch; - final provider = TaskProviderParsing.fromStorageValue(source.provider); + final provider = BusyProviderCodec.requireStorageValue(source.provider); final localEventId = 'local:${const Uuid().v4()}'; final startTimeZone = _effectiveStartTimeZone( draft, @@ -627,6 +643,9 @@ class CalendarRepository { 'supported.', ); } + if (source.davCollectionId != null) { + return _updateLocalDavEvent(source, existing, draft); + } if (draft.recurrenceChanged && existing.providerRecurringEventId != null) { throw UnsupportedError( 'Editing a recurring series from an individual occurrence is not ' @@ -634,7 +653,7 @@ class CalendarRepository { ); } final now = _now().millisecondsSinceEpoch; - final provider = TaskProviderParsing.fromStorageValue(source.provider); + final provider = BusyProviderCodec.requireStorageValue(source.provider); final startTimeZone = _effectiveStartTimeZone( draft, source.timeZone, @@ -758,7 +777,10 @@ class CalendarRepository { return edits.isEmpty ? null : edits.first; } - Future deleteLocalEvent(String eventId) async { + Future deleteLocalEvent( + String eventId, { + RecurringEventMutationScope? recurringScope, + }) async { final existing = await (_database.select( _database.calendarEvents, )..where((row) => row.id.equals(eventId))).getSingle(); @@ -769,6 +791,13 @@ class CalendarRepository { source, operation: CalendarMutationOperation.deleteEvent, ); + if (source.davCollectionId != null) { + return _deleteLocalDavEvent( + source, + existing, + recurringScope: recurringScope, + ); + } final now = _now().millisecondsSinceEpoch; await _database.transaction(() async { await (_database.update( @@ -808,6 +837,492 @@ class CalendarRepository { return existing.accountId; } + Future _createLocalDavEvent( + CalendarSource source, + EventEditorDraft draft, + ) async { + _requireDavSchedulingUnchanged(draft, creating: true); + final start = draft.start; + final end = draft.end; + final collectionId = source.davCollectionId; + if (start == null || + end == null || + collectionId == null || + !draft.canSave) { + throw ArgumentError( + 'A valid DAV event range and collection are required.', + ); + } + final startTimeZone = _effectiveStartTimeZone( + draft, + source.timeZone, + _localTimeZone, + ); + final endTimeZone = _effectiveEndTimeZone( + draft, + source.timeZone, + startTimeZone, + _localTimeZone, + ); + final object = buildDavEventObject( + _davEventInput( + draft, + start: start, + end: end, + startTimeZone: startTimeZone, + endTimeZone: endTimeZone, + ), + nowUtc: () => _now().toUtc(), + ); + final projectionId = 'dav-local-event-${const Uuid().v4()}'; + final now = _now(); + final nowMillis = now.millisecondsSinceEpoch; + final projectionJson = jsonEncode({ + 'transport': 'caldav', + 'uid': object.uid, + 'localPendingCreate': true, + }); + await _database.transaction(() async { + await _database + .into(_database.calendarEvents) + .insert( + CalendarEventsCompanion.insert( + id: projectionId, + accountId: draft.accountId, + calendarSourceId: source.id, + provider: source.provider, + providerCalendarId: source.providerCalendarId, + providerEventId: object.uid, + davCollectionId: Value(collectionId), + icalUid: Value(object.uid), + occurrenceKey: Value(object.uid), + providerRecurringEventId: Value( + _hasDavRecurrence(draft.recurrence) ? object.uid : null, + ), + title: draft.title.trim(), + description: Value(draft.description), + location: Value(draft.location), + allDay: Value(draft.allDay), + startDate: Value(draft.allDay ? _date(start) : null), + startDateTime: Value( + draft.allDay ? null : start.toIso8601String(), + ), + startTimeZone: Value(startTimeZone), + endDate: Value(draft.allDay ? _date(end) : null), + endDateTime: Value(draft.allDay ? null : end.toIso8601String()), + endTimeZone: Value(endTimeZone), + recurrenceJson: Value(_json(draft.recurrence)), + remindersJson: Value(_json(draft.reminders)), + attendeesJson: const Value(null), + categoriesJson: Value(_json(draft.categories)), + visibility: Value(draft.visibilityOrSensitivity), + transparencyOrShowAs: Value(draft.showAs), + isDeleted: const Value(false), + rawJson: Value(projectionJson), + baselineRawJson: Value(projectionJson), + createdAtLocal: nowMillis, + updatedAtLocal: nowMillis, + syncStatus: const Value('pending'), + ), + ); + await DavPendingOperationQueue( + database: _database, + nowUtc: () => _now().toUtc(), + ).enqueueCreate( + accountId: draft.accountId, + collectionId: collectionId, + object: object, + localProjectionId: projectionId, + ); + }); + await _notificationScheduleService().rebuildUpcomingEventNotifications( + draft.accountId, + ); + await _onNotificationScheduleChanged?.call(); + } + + Future _updateLocalDavEvent( + CalendarSource source, + CalendarEvent existing, + EventEditorDraft draft, + ) async { + _requireDavSchedulingUnchanged(draft, creating: false); + final collectionId = source.davCollectionId; + final uid = existing.icalUid; + final start = draft.start; + final end = draft.end; + if (collectionId == null || uid == null || start == null || end == null) { + throw StateError('The DAV event projection is incomplete.'); + } + final recurring = existing.providerRecurringEventId != null; + final recurringScope = draft.recurringMutationScope; + if (recurring && + (recurringScope == null || + recurringScope == RecurringEventMutationScope.thisAndFuture)) { + throw UnsupportedError( + 'A supported recurring-event editing scope is required.', + ); + } + final startTimeZone = _effectiveStartTimeZone( + draft, + source.timeZone, + _localTimeZone, + ); + final endTimeZone = _effectiveEndTimeZone( + draft, + source.timeZone, + startTimeZone, + _localTimeZone, + ); + var input = _davEventInput( + draft, + start: start, + end: end, + startTimeZone: startTimeZone, + endTimeZone: endTimeZone, + ); + final target = IcalComponentKey( + componentType: 'VEVENT', + uid: uid, + recurrenceIdKey: existing.recurrenceIdKey, + ); + final queue = DavPendingOperationQueue( + database: _database, + nowUtc: () => _now().toUtc(), + ); + DavMutationPatch? patch; + late final String baselineRawIcs; + final objectId = existing.davObjectId; + if (objectId == null) { + final create = await _pendingDavCreateForProjection(existing.id); + if (create == null) { + throw StateError('The pending DAV event create is unavailable.'); + } + baselineRawIcs = _pendingCreateRawIcs(create); + } else { + baselineRawIcs = await queue.editableRawIcsForObject( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + ); + } + if (recurringScope == RecurringEventMutationScope.entireSeries) { + input = _seriesDavEventInput( + baselineRawIcs: baselineRawIcs, + uid: uid, + existing: existing, + desired: input, + ); + patch = buildDavEventUpdatePatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + baselineRawIcs: baselineRawIcs, + input: input, + ); + } else if (recurringScope == RecurringEventMutationScope.singleOccurrence && + existing.recurrenceIdKey == null) { + final occurrenceKey = existing.occurrenceKey; + if (occurrenceKey == null || objectId == null) { + throw UnsupportedError( + 'One-occurrence editing requires a synchronized occurrence.', + ); + } + patch = buildDavEventOccurrenceExceptionPatch( + uid: uid, + occurrenceKey: occurrenceKey, + baselineRawIcs: baselineRawIcs, + input: input, + nowUtc: () => _now().toUtc(), + ); + } else { + patch = buildDavEventUpdatePatch( + target: target, + baselineRawIcs: baselineRawIcs, + input: input, + ); + } + if (patch == null) return; + final candidate = patch.applyTo(baselineRawIcs, nowUtc: _now().toUtc()); + + await _database.transaction(() async { + if (objectId == null) { + final updated = await queue.updateUnsentCreate( + accountId: existing.accountId, + collectionId: collectionId, + localProjectionId: existing.id, + patch: patch!, + ); + if (!updated) { + throw StateError( + 'The pending DAV event create is no longer editable.', + ); + } + } else { + await queue.enqueueUpdate( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + patch: patch!, + ); + } + if (objectId == null) { + await _writeDavEventProjection( + existing.id, + draft, + startTimeZone: startTimeZone, + endTimeZone: endTimeZone, + ); + } else { + await DavObjectRepository( + database: _database, + ).projectLocalMutationCandidate( + accountId: existing.accountId, + collectionId: collectionId, + provider: BusyProviderCodec.requireStorageValue(source.provider), + objectId: objectId, + candidateRawIcs: candidate, + projectedAtUtc: _now().toUtc(), + ); + } + }); + await _notificationScheduleService().rebuildUpcomingEventNotifications( + existing.accountId, + ); + await _onNotificationScheduleChanged?.call(); + } + + Future _deleteLocalDavEvent( + CalendarSource source, + CalendarEvent existing, { + RecurringEventMutationScope? recurringScope, + }) async { + final collectionId = source.davCollectionId; + final uid = existing.icalUid; + if (collectionId == null || uid == null) { + throw StateError('The DAV event projection is incomplete.'); + } + final recurring = existing.providerRecurringEventId != null; + if (recurring && + (recurringScope == null || + recurringScope == RecurringEventMutationScope.thisAndFuture)) { + throw UnsupportedError( + 'A supported recurring-event deletion scope is required.', + ); + } + final queue = DavPendingOperationQueue( + database: _database, + nowUtc: () => _now().toUtc(), + ); + final objectId = existing.davObjectId; + await _database.transaction(() async { + if (objectId == null) { + if (recurringScope == RecurringEventMutationScope.singleOccurrence) { + throw UnsupportedError( + 'One-occurrence deletion requires a synchronized occurrence.', + ); + } + final cancelled = await queue.cancelUnsentCreate( + accountId: existing.accountId, + collectionId: collectionId, + localProjectionId: existing.id, + ); + if (!cancelled) { + throw StateError( + 'The DAV event create may already be in progress and cannot be ' + 'cancelled locally.', + ); + } + await (_database.delete( + _database.calendarEvents, + )..where((row) => row.id.equals(existing.id))).go(); + return; + } + + final object = await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (object == null || object.collectionId != collectionId) { + throw StateError('The DAV event baseline is unavailable.'); + } + final provider = BusyProviderCodec.requireStorageValue(source.provider); + if (recurringScope == RecurringEventMutationScope.singleOccurrence) { + final occurrenceKey = existing.occurrenceKey; + if (occurrenceKey == null) { + throw StateError('The DAV occurrence identity is unavailable.'); + } + final editableRawIcs = await queue.editableRawIcsForObject( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + ); + final patch = buildDavEventOccurrenceCancellationPatch( + uid: uid, + occurrenceKey: occurrenceKey, + baselineRawIcs: editableRawIcs, + nowUtc: () => _now().toUtc(), + ); + final candidate = patch.applyTo(editableRawIcs, nowUtc: _now().toUtc()); + await queue.enqueueUpdate( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + patch: patch, + ); + await DavObjectRepository( + database: _database, + ).projectLocalMutationCandidate( + accountId: existing.accountId, + collectionId: collectionId, + provider: provider, + objectId: objectId, + candidateRawIcs: candidate, + projectedAtUtc: _now().toUtc(), + ); + return; + } + + final semantic = IcalSemanticDocument.parse(object.rawIcsBody); + final targets = [ + for (final component in semantic.components) + if (component.componentType == 'VEVENT' && + component.uid == uid && + (recurringScope == RecurringEventMutationScope.entireSeries || + component.recurrenceIdKey == existing.recurrenceIdKey)) + IcalComponentKey( + componentType: component.componentType, + uid: uid, + recurrenceIdKey: component.recurrenceIdKey, + ), + ]; + if (targets.isEmpty) { + throw StateError('The DAV event component is unavailable.'); + } + final targetIdentities = targets.map(_icalComponentIdentity).toSet(); + final targetDocumentComponents = { + for (final component in semantic.components) + if (targetIdentities.contains( + _icalComponentIdentity( + IcalComponentKey( + componentType: component.componentType, + uid: component.uid!, + recurrenceIdKey: component.recurrenceIdKey, + ), + ), + )) + component.documentComponent, + }; + final hasUntargetedCalendarComponent = semantic + .document + .calendarComponents + .any( + (component) => + component.name != 'VTIMEZONE' && + !targetDocumentComponents.contains(component), + ); + if (!hasUntargetedCalendarComponent) { + await queue.enqueueDelete( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + target: targets.first, + scope: recurring + ? DavMutationScope.recurrenceMaster + : DavMutationScope.object, + ); + await (_database.update( + _database.calendarEvents, + )..where((row) => row.davObjectId.equals(objectId))).write( + CalendarEventsCompanion( + isDeleted: const Value(true), + syncStatus: const Value('pending'), + updatedAtLocal: Value(_now().millisecondsSinceEpoch), + ), + ); + return; + } + final patch = buildDavComponentRemovalPatch( + targets: targets, + scope: recurring + ? DavMutationScope.recurrenceMaster + : DavMutationScope.object, + ); + final candidate = patch.applyTo( + object.rawIcsBody, + nowUtc: _now().toUtc(), + ); + await queue.enqueueUpdate( + accountId: existing.accountId, + collectionId: collectionId, + objectId: objectId, + patch: patch, + ); + await DavObjectRepository( + database: _database, + ).projectLocalMutationCandidate( + accountId: existing.accountId, + collectionId: collectionId, + provider: provider, + objectId: objectId, + candidateRawIcs: candidate, + projectedAtUtc: _now().toUtc(), + ); + }); + await _notificationScheduleService().rebuildUpcomingEventNotifications( + existing.accountId, + ); + await _onNotificationScheduleChanged?.call(); + return existing.accountId; + } + + Future _pendingDavCreateForProjection(String projectionId) { + return (_database.select(_database.pendingOps) + ..where( + (row) => + row.eventId.equals(projectionId) & + row.operationType.equals('dav.create') & + row.state.equals('pending') & + row.attemptCount.equals(0), + ) + ..limit(1)) + .getSingleOrNull(); + } + + Future _writeDavEventProjection( + String eventId, + EventEditorDraft draft, { + required String? startTimeZone, + required String? endTimeZone, + }) { + return (_database.update( + _database.calendarEvents, + )..where((row) => row.id.equals(eventId))).write( + CalendarEventsCompanion( + title: Value(draft.title.trim()), + description: Value(draft.description), + location: Value(draft.location), + allDay: Value(draft.allDay), + startDate: Value(draft.allDay ? _date(draft.start) : null), + startDateTime: Value( + draft.allDay ? null : draft.start?.toIso8601String(), + ), + startTimeZone: Value(startTimeZone), + endDate: Value(draft.allDay ? _date(draft.end) : null), + endDateTime: Value(draft.allDay ? null : draft.end?.toIso8601String()), + endTimeZone: Value(endTimeZone), + recurrenceJson: draft.recurrenceChanged + ? Value(_json(draft.recurrence)) + : const Value.absent(), + remindersJson: Value(_json(draft.reminders)), + categoriesJson: draft.categoriesChanged + ? Value(_json(draft.categories)) + : const Value.absent(), + visibility: Value(draft.visibilityOrSensitivity), + transparencyOrShowAs: Value(draft.showAs), + updatedAtLocal: Value(_now().millisecondsSinceEpoch), + syncStatus: const Value('pending'), + ), + ); + } + NotificationScheduleService _notificationScheduleService() { return NotificationScheduleService( database: _database, @@ -873,14 +1388,14 @@ class CalendarRepository { } final source = sourceId( accountId: accountId, - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: providerCalendarId, ); await (_database.update(_database.calendarEvents)..where( (row) => row.accountId.equals(accountId) & row.calendarSourceId.equals(source) & - row.provider.equals(TaskProvider.google.storageValue) & + row.provider.equals(BusyProvider.google.storageValue) & row.providerEventId.isIn(providerRecurringEventIds) & row.providerRecurringEventId.isNull() & row.syncStatus.equals('synced') & @@ -895,7 +1410,7 @@ class CalendarRepository { ); } - Future syncState({ + Future syncState({ required String accountId, required BusyProvider provider, required String syncKind, @@ -908,7 +1423,7 @@ class CalendarRepository { calendarSourceId: calendarSourceId, ); return (_database.select( - _database.calendarSyncStates, + _database.syncCursors, )..where((row) => row.id.equals(id))).getSingleOrNull(); } @@ -961,6 +1476,170 @@ void _requireWritableSource( throw CalendarMutationNotAllowed(operation: operation, sourceId: source.id); } +void _requireDavSchedulingUnchanged( + EventEditorDraft draft, { + required bool creating, +}) { + final changesAttendees = creating + ? draft.attendees.isNotEmpty + : draft.attendeesChanged; + if (changesAttendees || + draft.createConference || + (creating && draft.conference != null)) { + throw UnsupportedError( + 'DAV attendee and scheduling mutations are disabled until scheduling ' + 'inbox/outbox interoperability is available.', + ); + } +} + +DavEventMutationInput _davEventInput( + EventEditorDraft draft, { + required DateTime start, + required DateTime end, + required String? startTimeZone, + required String? endTimeZone, +}) { + return DavEventMutationInput( + title: draft.title, + allDay: draft.allDay, + start: start, + end: end, + startTimeZone: startTimeZone, + endTimeZone: endTimeZone, + description: draft.description, + location: draft.location, + recurrence: draft.recurrence, + recurrenceChanged: draft.recurrenceChanged, + reminders: draft.reminders, + categories: draft.categories, + categoriesChanged: draft.categoriesChanged, + classification: draft.visibilityOrSensitivity, + transparency: draft.showAs, + ); +} + +DavEventMutationInput _seriesDavEventInput({ + required String baselineRawIcs, + required String uid, + required CalendarEvent existing, + required DavEventMutationInput desired, +}) { + final semantic = IcalSemanticDocument.parse(baselineRawIcs); + final masters = semantic.components.where( + (component) => + component.componentType == 'VEVENT' && + component.uid == uid && + component.recurrenceIdKey == null, + ); + if (masters.length != 1 || masters.single.start == null) { + throw StateError('The DAV recurrence master is unavailable.'); + } + final master = masters.single; + final masterStart = _editableIcalTemporal(master.start!); + final masterEnd = master.end == null + ? masterStart.add(master.duration?.duration ?? const Duration(hours: 1)) + : _editableIcalTemporal(master.end!); + final projectedStart = DateTime.tryParse( + existing.allDay ? existing.startDate ?? '' : existing.startDateTime ?? '', + ); + final projectedEnd = DateTime.tryParse( + existing.allDay ? existing.endDate ?? '' : existing.endDateTime ?? '', + ); + if (projectedStart == null || projectedEnd == null) { + throw StateError('The DAV occurrence projection is incomplete.'); + } + final startChanged = desired.start != projectedStart; + final endChanged = desired.end != projectedEnd; + final allDayChanged = desired.allDay != existing.allDay; + final seriesStart = startChanged + ? masterStart.add(desired.start.difference(projectedStart)) + : masterStart; + final seriesEnd = endChanged + ? masterEnd.add(desired.end.difference(projectedEnd)) + : masterEnd; + final masterTimeZone = _icalTimeZone(master.start!); + final masterEndTimeZone = master.end == null + ? masterTimeZone + : _icalTimeZone(master.end!); + return DavEventMutationInput( + title: desired.title != existing.title + ? desired.title + : master.summary ?? '', + allDay: allDayChanged ? desired.allDay : master.start!.isDate, + start: seriesStart, + end: seriesEnd, + startTimeZone: startChanged || allDayChanged + ? desired.startTimeZone + : masterTimeZone, + endTimeZone: endChanged || allDayChanged + ? desired.endTimeZone + : masterEndTimeZone, + description: (desired.description ?? '') != (existing.description ?? '') + ? desired.description + : master.description, + location: (desired.location ?? '') != (existing.location ?? '') + ? desired.location + : master.location, + recurrence: desired.recurrence, + recurrenceChanged: desired.recurrenceChanged, + reminders: desired.reminders, + categories: desired.categories, + categoriesChanged: desired.categoriesChanged, + classification: + (desired.classification ?? '') != (existing.visibility ?? '') + ? desired.classification + : master.classification, + transparency: + (desired.transparency ?? '') != (existing.transparencyOrShowAs ?? '') + ? desired.transparency + : master.transparency, + ); +} + +DateTime _editableIcalTemporal(IcalTemporalValue value) { + final wall = value.localValue; + if (value.kind == IcalTemporalKind.utcDateTime) return wall.toUtc(); + return DateTime( + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ); +} + +String? _icalTimeZone(IcalTemporalValue value) => switch (value.kind) { + IcalTemporalKind.utcDateTime => 'UTC', + IcalTemporalKind.tzidDateTime => value.timeZoneId, + IcalTemporalKind.date || IcalTemporalKind.floatingDateTime => null, +}; + +String _icalComponentIdentity(IcalComponentKey key) => + '${key.componentType.toUpperCase()}\u0000${key.uid}\u0000' + '${key.recurrenceIdKey ?? ''}'; + +bool _hasDavRecurrence(Object? recurrence) { + if (recurrence is List) return recurrence.isNotEmpty; + if (recurrence is Map && recurrence['rules'] is List) { + return (recurrence['rules']! as List).isNotEmpty; + } + return false; +} + +String _pendingCreateRawIcs(PendingOp operation) { + try { + final decoded = jsonDecode(operation.requestJson); + if (decoded is Map && decoded['rawIcs'] is String) { + return decoded['rawIcs']! as String; + } + } on FormatException { + // Invalid pending payloads use the same local-state error. + } + throw StateError('The pending DAV event body is invalid.'); +} + String? _json(Object? value) => value == null ? null : jsonEncode(value); Map _eventRequest( @@ -996,10 +1675,10 @@ Map _eventRequest( 'colorId': draft.colorId, if (isCreate || draft.categoriesChanged) 'categoriesJson': _categoriesJson(draft, provider), - 'visibility': provider == TaskProvider.google + 'visibility': provider == BusyProvider.google ? draft.visibilityOrSensitivity : null, - 'sensitivity': provider == TaskProvider.microsoft + 'sensitivity': provider == BusyProvider.microsoft ? draft.visibilityOrSensitivity : null, 'transparencyOrShowAs': draft.showAs, @@ -1081,14 +1760,14 @@ Object? _attendeesJson(EventEditorDraft draft, BusyProvider provider) { } return [ for (final attendee in draft.attendees) - provider == TaskProvider.microsoft + provider == BusyProvider.microsoft ? attendee.toMicrosoftJson() : attendee.toGoogleJson(), ]; } Object? _categoriesJson(EventEditorDraft draft, BusyProvider provider) { - if (provider != TaskProvider.microsoft) { + if (provider != BusyProvider.microsoft) { return null; } return draft.categories; diff --git a/lib/src/features/calendar/presentation/event_description_editor.dart b/lib/src/features/calendar/presentation/event_description_editor.dart index 92f09d5..8791864 100644 --- a/lib/src/features/calendar/presentation/event_description_editor.dart +++ b/lib/src/features/calendar/presentation/event_description_editor.dart @@ -4,7 +4,7 @@ import '../../../app/busymax_design.dart'; import '../../../app/busymax_yaru_theme.dart'; import '../../../calendar_providers/calendar_description.dart'; import '../../../l10n/l10n.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; class EventDescriptionValue { const EventDescriptionValue({ @@ -42,7 +42,7 @@ class _EventDescriptionEditorState extends State { late final _RichDescriptionController _controller; var _notifying = false; - bool get _supportsRichText => widget.provider == TaskProvider.microsoft; + bool get _supportsRichText => widget.provider == BusyProvider.microsoft; @override void initState() { diff --git a/lib/src/features/calendar/presentation/event_editor.dart b/lib/src/features/calendar/presentation/event_editor.dart index 0779504..1eb0f99 100644 --- a/lib/src/features/calendar/presentation/event_editor.dart +++ b/lib/src/features/calendar/presentation/event_editor.dart @@ -10,7 +10,8 @@ import '../../../calendar_providers/calendar_colors.dart'; import '../../../l10n/l10n.dart'; import '../../../platform/linux_header_bar_service.dart'; import '../../../schedule/schedule_projection.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import '../../accounts/data/accounts_repository.dart'; import '../../tasks/presentation/desktop_date_time_fields.dart'; import '../data/calendar_repository.dart'; import 'event_description_editor.dart'; @@ -20,6 +21,7 @@ Future showBusyMaxEventEditorDialog( BuildContext context, { required EventEditorDraft initialDraft, required List sources, + required List accounts, LinuxHeaderBarService? headerBarService, bool allowDelete = true, Map> categorySuggestionsByAccount = const {}, @@ -33,15 +35,16 @@ Future showBusyMaxEventEditorDialog( return EventEditor( initialDraft: initialDraft, sources: sources, + accounts: accounts, categorySuggestionsByAccount: categorySuggestionsByAccount, headerBarService: headerBarService, onCancel: () => Navigator.of(context).pop(), onSave: (draft) => Navigator.of(context).pop(EventEditorDialogResult.save(draft)), onDelete: allowDelete && initialDraft.eventId != null - ? (eventId) => Navigator.of( + ? (eventId, scope) => Navigator.of( context, - ).pop(EventEditorDialogResult.delete(eventId)) + ).pop(EventEditorDialogResult.delete(eventId, scope: scope)) : null, ); }, @@ -49,20 +52,34 @@ Future showBusyMaxEventEditorDialog( } class EventEditorDialogResult { - const EventEditorDialogResult._({this.draft, this.deletedEventId}); + const EventEditorDialogResult._({ + this.draft, + this.deletedEventId, + this.deletionScope, + }); factory EventEditorDialogResult.save(EventEditorDraft draft) { return EventEditorDialogResult._(draft: draft); } - factory EventEditorDialogResult.delete(String eventId) { - return EventEditorDialogResult._(deletedEventId: eventId); + factory EventEditorDialogResult.delete( + String eventId, { + RecurringEventMutationScope? scope, + }) { + return EventEditorDialogResult._( + deletedEventId: eventId, + deletionScope: scope, + ); } final EventEditorDraft? draft; final String? deletedEventId; + final RecurringEventMutationScope? deletionScope; } +typedef EventEditorDeleteCallback = + void Function(String eventId, RecurringEventMutationScope? scope); + class EventEditor extends StatefulWidget { const EventEditor({ super.key, @@ -70,6 +87,7 @@ class EventEditor extends StatefulWidget { required this.sources, required this.onCancel, required this.onSave, + this.accounts = const [], this.onDelete, this.categorySuggestionsByAccount = const {}, this.headerBarService, @@ -77,10 +95,11 @@ class EventEditor extends StatefulWidget { final EventEditorDraft initialDraft; final List sources; + final List accounts; final Map> categorySuggestionsByAccount; final VoidCallback onCancel; final ValueChanged onSave; - final ValueChanged? onDelete; + final EventEditorDeleteCallback? onDelete; final LinuxHeaderBarService? headerBarService; @override @@ -115,6 +134,9 @@ class _EventEditorState extends State { Widget build(BuildContext context) { final l10n = context.l10n; final dirty = _hasUnsavedChanges; + final title = widget.initialDraft.eventId == null + ? l10n.newEvent + : l10n.editEvent; CalendarSourceEntity? currentSource; for (final source in widget.sources) { if (source.id == _draft.sourceId) { @@ -122,12 +144,42 @@ class _EventEditorState extends State { break; } } - final provider = currentSource?.provider ?? TaskProvider.google; - final title = widget.initialDraft.eventId == null - ? l10n.newEvent - : l10n.editEvent; + if (currentSource == null) { + return BusyMaxModalEditorScaffold( + title: title, + cancelLabel: l10n.cancel, + saveLabel: l10n.save, + onCancel: widget.onCancel, + onSave: null, + children: [ + BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxActionRow( + title: l10n.calendar, + subtitle: l10n.noCalendarsSynced, + leading: const Icon(YaruIcons.calendar), + enabled: false, + ), + ], + ), + ], + ); + } + final provider = currentSource.provider; + final schedulingReadOnly = + provider == BusyProvider.appleICloud || + provider == BusyProvider.nextcloud; + final davRecurring = + schedulingReadOnly && _draft.providerRecurringEventId != null; final timeFieldsValid = _draft.allDay || (_startTimeValid && _endTimeValid); - final canSave = dirty && _draft.canSave && timeFieldsValid; + final recurringScopeValid = + !davRecurring || + (_draft.recurringMutationScope != null && + _draft.recurringMutationScope != + RecurringEventMutationScope.thisAndFuture); + final canSave = + dirty && _draft.canSave && timeFieldsValid && recurringScopeValid; return CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.escape): () { @@ -184,7 +236,10 @@ class _EventEditorState extends State { ), ], ), - BusyMaxGroupedList(filled: true, children: [_calendarRow()]), + BusyMaxGroupedList( + filled: true, + children: [_accountRow(), _calendarRow()], + ), BusyMaxGroupedList( filled: true, children: [ @@ -260,6 +315,12 @@ class _EventEditorState extends State { ), ], ), + if (davRecurring) + BusyMaxGroupedList( + title: l10n.recurringEventScope, + filled: true, + children: _recurringScopeRows(), + ), if (_draft.providerRecurringEventId == null) BusyMaxGroupedList( filled: true, @@ -270,12 +331,13 @@ class _EventEditorState extends State { filled: true, children: _reminderRows(provider), ), - BusyMaxGroupedList( - title: l10n.guests, - filled: true, - children: _guestRows(), - ), - if (provider == TaskProvider.microsoft) + if (!schedulingReadOnly || _draft.attendees.isNotEmpty) + BusyMaxGroupedList( + title: l10n.guests, + filled: true, + children: _guestRows(readOnly: schedulingReadOnly), + ), + if (provider == BusyProvider.microsoft || schedulingReadOnly) BusyMaxGroupedList( title: l10n.organizationSection, filled: true, @@ -330,7 +392,9 @@ class _EventEditorState extends State { ), ), destructive: true, - onTap: _deleteCurrentEvent, + onTap: davRecurring && _draft.recurringMutationScope == null + ? null + : _deleteCurrentEvent, ), ], ), @@ -384,7 +448,9 @@ class _EventEditorState extends State { } bool get _canDeleteWithShortcut { - return _draft.eventId != null && widget.onDelete != null; + return _draft.eventId != null && + widget.onDelete != null && + (!_requiresRecurringScope || _draft.recurringMutationScope != null); } bool _isEditableTextFocused() { @@ -399,8 +465,59 @@ class _EventEditorState extends State { void _deleteCurrentEvent() { final eventId = _draft.eventId; if (eventId != null && widget.onDelete != null) { - widget.onDelete!(eventId); + widget.onDelete!(eventId, _draft.recurringMutationScope); + } + } + + bool get _requiresRecurringScope { + if (_draft.providerRecurringEventId == null) return false; + for (final source in widget.sources) { + if (source.id != _draft.sourceId) continue; + return source.provider == BusyProvider.appleICloud || + source.provider == BusyProvider.nextcloud; } + return false; + } + + List _recurringScopeRows() { + final l10n = context.l10n; + return [ + BusyMaxActionRow( + title: l10n.entireSeries, + subtitle: l10n.chooseRecurringEventScope, + leading: Icon( + _draft.recurringMutationScope == + RecurringEventMutationScope.entireSeries + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => setState(() { + _draft = _draft.copyWith( + recurringMutationScope: RecurringEventMutationScope.entireSeries, + ); + }), + ), + BusyMaxActionRow( + title: l10n.singleOccurrence, + leading: Icon( + _draft.recurringMutationScope == + RecurringEventMutationScope.singleOccurrence + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + ), + onTap: () => setState(() { + _draft = _draft.copyWith( + recurringMutationScope: + RecurringEventMutationScope.singleOccurrence, + ); + }), + ), + BusyMaxActionRow( + title: l10n.thisAndFutureUnavailable, + leading: const Icon(Icons.update_disabled_outlined), + enabled: false, + ), + ]; } Widget _repeatRow(BusyProvider provider) { @@ -430,16 +547,46 @@ class _EventEditorState extends State { ); } + Widget _accountRow() { + final existingAccountId = widget.initialDraft.eventId == null + ? null + : widget.initialDraft.accountId; + final accountIds = + { + for (final source in widget.sources) + if (existingAccountId == null || + source.accountId == existingAccountId) + source.accountId, + }.toList()..sort((first, second) { + final labelOrder = _accountLabel( + first, + ).toLowerCase().compareTo(_accountLabel(second).toLowerCase()); + return labelOrder != 0 ? labelOrder : first.compareTo(second); + }); + final selected = accountIds.contains(_draft.accountId) + ? _draft.accountId + : accountIds.first; + return BusyMaxComboRow( + title: context.l10n.account, + leading: const Icon(YaruIcons.user), + values: accountIds, + selected: selected, + enabled: existingAccountId == null, + labelFor: _accountLabel, + onSelected: _selectAccount, + ); + } + Widget _calendarRow() { final existingSourceId = widget.initialDraft.eventId == null ? null : widget.initialDraft.sourceId; - final sources = existingSourceId == null - ? widget.sources - : [ - for (final source in widget.sources) - if (source.id == existingSourceId) source, - ]; + final sources = [ + for (final source in widget.sources) + if (source.accountId == _draft.accountId && + (existingSourceId == null || source.id == existingSourceId)) + source, + ]..sort(_compareCalendarSources); if (sources.isEmpty) { return BusyMaxActionRow( title: context.l10n.calendar, @@ -449,49 +596,92 @@ class _EventEditorState extends State { } final selected = sources.any((source) => source.id == _draft.sourceId) ? _draft.sourceId - : sources.first.id; + : _preferredCalendarSource(sources).id; + final sourcesById = {for (final source in sources) source.id: source}; return BusyMaxComboRow( title: context.l10n.calendar, leading: const Icon(YaruIcons.calendar), values: [for (final source in sources) source.id], selected: selected, enabled: existingSourceId == null, - labelFor: (value) => - sources.firstWhere((source) => source.id == value).summary, + labelFor: (value) => sourcesById[value]!.summary, selectorLeadingBuilder: (context, value) { - final source = sources.firstWhere((source) => source.id == value); + final source = sourcesById[value]!; return _CalendarSourceDot(color: _calendarSourceColor(context, source)); }, - onSelected: (value) { - final source = sources.firstWhere((source) => source.id == value); - final recurrenceType = _recurrenceType(_draft.recurrence); - final adjustedRecurrence = - _draft.recurrenceChanged && recurrenceType != 'none' - ? _recurrenceFor(source.provider, recurrenceType, _draft.start) - : null; - setState(() { - if (source.provider != TaskProvider.microsoft) { - _addingCategory = false; - } - _draft = _draft.copyWith( - accountId: source.accountId, - sourceId: source.id, - providerCalendarId: source.providerCalendarId, - categories: source.provider == TaskProvider.microsoft - ? _draft.categories - : const [], - recurrence: adjustedRecurrence, - ); - }); - }, + onSelected: (value) => _selectCalendarSource(sourcesById[value]!), ); } + int _compareCalendarSources( + CalendarSourceEntity first, + CalendarSourceEntity second, + ) { + final summaryOrder = first.summary.toLowerCase().compareTo( + second.summary.toLowerCase(), + ); + if (summaryOrder != 0) return summaryOrder; + return first.id.compareTo(second.id); + } + + String _accountLabel(String accountId) { + for (final candidate in widget.accounts) { + if (candidate.id == accountId) { + return candidate.selectorLabel; + } + } + for (final source in widget.sources) { + if (source.accountId == accountId) return source.provider.displayName; + } + return context.l10n.account; + } + + void _selectAccount(String accountId) { + if (accountId == _draft.accountId) return; + final sources = [ + for (final source in widget.sources) + if (source.accountId == accountId) source, + ]..sort(_compareCalendarSources); + if (sources.isEmpty) return; + _selectCalendarSource(_preferredCalendarSource(sources)); + } + + CalendarSourceEntity _preferredCalendarSource( + List sources, + ) { + for (final source in sources) { + if (source.primaryCalendar) return source; + } + return sources.first; + } + + void _selectCalendarSource(CalendarSourceEntity source) { + final recurrenceType = _recurrenceType(_draft.recurrence); + final adjustedRecurrence = + _draft.recurrenceChanged && recurrenceType != 'none' + ? _recurrenceFor(source.provider, recurrenceType, _draft.start) + : null; + setState(() { + if (source.provider == BusyProvider.google) { + _addingCategory = false; + } + _draft = _draft.copyWith( + accountId: source.accountId, + sourceId: source.id, + providerCalendarId: source.providerCalendarId, + categories: source.provider != BusyProvider.google + ? _draft.categories + : const [], + recurrence: adjustedRecurrence, + ); + }); + } + List _reminderRows(BusyProvider provider) { final l10n = context.l10n; final colorScheme = Theme.of(context).colorScheme; final minutes = _reminderMinutesList(_draft.reminders); - final supportsMultiple = provider == TaskProvider.google; + final supportsMultiple = provider != BusyProvider.microsoft; final canAddReminder = minutes.isEmpty || (supportsMultiple && @@ -552,29 +742,34 @@ class _EventEditorState extends State { ]; } - List _guestRows() { + List _guestRows({required bool readOnly}) { final rows = [ for (final attendee in _draft.attendees) BusyMaxActionRow( title: attendee.email, subtitle: attendee.displayName, leading: const Icon(Icons.person_outline), - trailing: YaruIconButton( - tooltip: MaterialLocalizations.of(context).deleteButtonTooltip, - icon: const Icon(YaruIcons.window_close), - onPressed: () { - setState(() { - _draft = _draft.copyWith( - attendees: [ - for (final item in _draft.attendees) - if (item != attendee) item, - ], - ); - }); - }, - ), + trailing: readOnly + ? null + : YaruIconButton( + tooltip: MaterialLocalizations.of( + context, + ).deleteButtonTooltip, + icon: const Icon(YaruIcons.window_close), + onPressed: () { + setState(() { + _draft = _draft.copyWith( + attendees: [ + for (final item in _draft.attendees) + if (item != attendee) item, + ], + ); + }); + }, + ), ), ]; + if (readOnly) return rows; if (!_addingGuest) { rows.add( BusyMaxActionRow( @@ -652,12 +847,12 @@ class _EventEditorState extends State { } Widget _availabilityRow(BusyProvider provider) { - final values = provider == TaskProvider.google + final values = provider != BusyProvider.microsoft ? const ['opaque', 'transparent'] : const ['free', 'tentative', 'busy', 'oof', 'workingElsewhere']; final selected = values.contains(_draft.showAs) ? _draft.showAs! - : provider == TaskProvider.google + : provider != BusyProvider.microsoft ? 'opaque' : 'busy'; return BusyMaxComboRow( @@ -675,7 +870,7 @@ class _EventEditorState extends State { } Widget _visibilityRow(BusyProvider provider) { - final values = provider == TaskProvider.google + final values = provider != BusyProvider.microsoft ? const ['default', 'public', 'private', 'confidential'] : const ['normal', 'personal', 'private', 'confidential']; final selected = values.contains(_draft.visibilityOrSensitivity) @@ -772,7 +967,7 @@ class _EventEditorState extends State { final end = _draft.end; final recurrenceType = _recurrenceType(_draft.recurrence); final adjustedRecurrence = - provider == TaskProvider.microsoft && + provider == BusyProvider.microsoft && _draft.recurrenceChanged && recurrenceType != 'none' ? _recurrenceFor(provider, recurrenceType, start) @@ -897,6 +1092,14 @@ String _recurrenceType(Object? recurrence) { if (value.contains('FREQ=YEARLY')) return 'yearly'; } if (recurrence is Map) { + final rules = recurrence['rules']; + if (rules is List && rules.isNotEmpty) { + final value = rules.first.toString().toUpperCase(); + if (value.contains('FREQ=DAILY')) return 'daily'; + if (value.contains('FREQ=WEEKLY')) return 'weekly'; + if (value.contains('FREQ=MONTHLY')) return 'monthly'; + if (value.contains('FREQ=YEARLY')) return 'yearly'; + } final pattern = recurrence['pattern']; if (pattern is Map) { final type = pattern['type']?.toString(); @@ -920,7 +1123,7 @@ Object _recurrenceFor(BusyProvider provider, String type, DateTime? start) { 'yearly' => 'YEARLY', _ => 'DAILY', }; - if (provider == TaskProvider.google) { + if (provider != BusyProvider.microsoft) { return ['RRULE:FREQ=$freq;INTERVAL=1']; } final recurrenceStart = start ?? DateTime.now(); @@ -991,7 +1194,7 @@ Object? _remindersFor(BusyProvider provider, List minutes) { if (normalized.isEmpty) { return null; } - if (provider == TaskProvider.google) { + if (provider != BusyProvider.microsoft) { return { 'useDefault': false, 'overrides': [ @@ -1004,7 +1207,7 @@ Object? _remindersFor(BusyProvider provider, List minutes) { } Object _disabledRemindersFor(BusyProvider provider) { - if (provider == TaskProvider.google) { + if (provider != BusyProvider.microsoft) { return {'useDefault': false, 'overrides': const []}; } return {'isReminderOn': false}; diff --git a/lib/src/features/calendar/presentation/event_editor_draft.dart b/lib/src/features/calendar/presentation/event_editor_draft.dart index e19ac89..e1599b7 100644 --- a/lib/src/features/calendar/presentation/event_editor_draft.dart +++ b/lib/src/features/calendar/presentation/event_editor_draft.dart @@ -1,3 +1,9 @@ +enum RecurringEventMutationScope { + entireSeries, + singleOccurrence, + thisAndFuture, +} + class EventAttendeeDraft { const EventAttendeeDraft({ required this.email, @@ -69,6 +75,7 @@ class EventEditorDraft { required this.allDay, this.eventId, this.providerRecurringEventId, + this.recurringMutationScope, this.start, this.end, this.startTimeZone, @@ -124,6 +131,7 @@ class EventEditorDraft { DateTime? end, String? location, String? providerRecurringEventId, + RecurringEventMutationScope? recurringMutationScope, String? description, String? descriptionContentType, String? descriptionHtml, @@ -149,6 +157,7 @@ class EventEditorDraft { sourceId: sourceId, providerCalendarId: providerCalendarId, providerRecurringEventId: providerRecurringEventId, + recurringMutationScope: recurringMutationScope, title: title, allDay: allDay, start: start, @@ -177,6 +186,7 @@ class EventEditorDraft { final String? eventId; final String? providerRecurringEventId; + final RecurringEventMutationScope? recurringMutationScope; final String accountId; final String sourceId; final String providerCalendarId; @@ -257,6 +267,7 @@ class EventEditorDraft { bool? responseRequested, bool? hideAttendees, bool? allowNewTimeProposals, + RecurringEventMutationScope? recurringMutationScope, bool clearLocation = false, bool clearDescription = false, bool clearRecurrence = false, @@ -266,10 +277,14 @@ class EventEditorDraft { bool clearVisibilityOrSensitivity = false, bool clearColorId = false, bool clearConference = false, + bool clearRecurringMutationScope = false, }) { return EventEditorDraft( eventId: eventId, providerRecurringEventId: providerRecurringEventId, + recurringMutationScope: clearRecurringMutationScope + ? null + : recurringMutationScope ?? this.recurringMutationScope, accountId: accountId ?? this.accountId, sourceId: sourceId ?? this.sourceId, providerCalendarId: providerCalendarId ?? this.providerCalendarId, @@ -318,6 +333,7 @@ class EventEditorDraft { return other is EventEditorDraft && other.eventId == eventId && other.providerRecurringEventId == providerRecurringEventId && + other.recurringMutationScope == recurringMutationScope && other.accountId == accountId && other.sourceId == sourceId && other.providerCalendarId == providerCalendarId && @@ -353,6 +369,7 @@ class EventEditorDraft { int get hashCode => Object.hashAll([ eventId, providerRecurringEventId, + recurringMutationScope, accountId, sourceId, providerCalendarId, diff --git a/lib/src/features/notifications/notification_schedule_service.dart b/lib/src/features/notifications/notification_schedule_service.dart index d1818a6..a403cc3 100644 --- a/lib/src/features/notifications/notification_schedule_service.dart +++ b/lib/src/features/notifications/notification_schedule_service.dart @@ -4,7 +4,7 @@ import 'package:drift/drift.dart'; import '../../core/time/provider_date_time.dart'; import '../../db/app_database.dart'; -import '../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../accounts/data/accounts_repository.dart'; class NotificationScheduleService { @@ -269,15 +269,15 @@ List _eventReminderMinutes(CalendarEvent event, {CalendarSource? source}) { if (raw == null || raw.isEmpty) { return const []; } - final provider = TaskProviderParsing.fromStorageValue(event.provider); + final provider = BusyProviderCodec.requireStorageValue(event.provider); final decoded = _decodeJson(raw); - if (provider == TaskProvider.microsoft && decoded is Map) { + if (provider == BusyProvider.microsoft && decoded is Map) { final map = decoded.cast(); final enabled = map['isReminderOn'] == true; final minutes = map['reminderMinutesBeforeStart']; return enabled && minutes is int ? [minutes] : const []; } - if (provider == TaskProvider.google && decoded is Map) { + if (provider == BusyProvider.google && decoded is Map) { final map = decoded.cast(); if (map['useDefault'] == true) { return _googleDefaultReminderMinutes(source); diff --git a/lib/src/features/schedule/presentation/schedule_agenda_view.dart b/lib/src/features/schedule/presentation/schedule_agenda_view.dart index d268b15..76eedac 100644 --- a/lib/src/features/schedule/presentation/schedule_agenda_view.dart +++ b/lib/src/features/schedule/presentation/schedule_agenda_view.dart @@ -3,8 +3,10 @@ import 'package:intl/intl.dart'; import 'package:yaru/yaru.dart'; import '../../../app/busymax_design.dart'; +import '../../../app/busymax_glyphs.dart'; import '../../../app/busymax_surface_colors.dart'; import '../../../l10n/l10n.dart'; +import '../../tasks/domain/task_checklist_item.dart'; import '../../../schedule/schedule_item.dart'; import '../../../schedule/schedule_projection.dart'; import '../../../schedule/schedule_range.dart'; @@ -19,6 +21,7 @@ class ScheduleAgendaView extends StatefulWidget { required this.items, required this.onItemSelected, required this.onTaskCompletionChanged, + this.onChecklistItemCompletionChanged, this.hasMoreOverdueTasks = false, this.hasMoreNoDateTasks = false, this.onLoadMore, @@ -32,6 +35,12 @@ class ScheduleAgendaView extends StatefulWidget { final ScheduleItemSelectionCallback onItemSelected; final void Function(TaskScheduleItem item, bool completed) onTaskCompletionChanged; + final void Function( + TaskScheduleItem parent, + TaskChecklistItemEntity item, + bool completed, + )? + onChecklistItemCompletionChanged; final bool hasMoreOverdueTasks; final bool hasMoreNoDateTasks; final VoidCallback? onLoadMore; @@ -83,11 +92,26 @@ class _ScheduleAgendaViewState extends State { !item.completed && ScheduleProjection.day(start).isBefore(rangeStart); }).toList()..sort(compareScheduleItems); + final orderedOverdueTasks = ScheduleProjection.arrangeHierarchy( + overdueTasks, + ); + final hierarchy = _AgendaHierarchyIndex(widget.items); + final emittedTasks = {}; + final overdueEntries = _entriesFor( + orderedOverdueTasks, + hierarchy, + emittedTasks, + ); + final noDateEntries = _entriesFor(noDateTasks, hierarchy, emittedTasks); final days = groups.keys .where((day) => !day.isBefore(rangeStart) && day.isBefore(rangeEnd)) .toList() ..sort(); + final dayEntries = >{ + for (final day in days) + day: _entriesFor(groups[day]!, hierarchy, emittedTasks), + }; return NotificationListener( onNotification: _handleScroll, @@ -101,20 +125,12 @@ class _ScheduleAgendaViewState extends State { BusyMaxSpacing.xl, ), children: [ - if (overdueTasks.isNotEmpty) + if (overdueEntries.isNotEmpty || widget.hasMoreOverdueTasks) BusyMaxGroupedList( title: context.l10n.overdue, filled: true, children: [ - for (final item in overdueTasks) - _AgendaRow( - item: item, - onAnchorAvailable: widget.onItemAnchorAvailable, - onTap: (context, [globalPosition]) => - widget.onItemSelected(context, item, globalPosition), - onTaskCompletionChanged: (completed) => - widget.onTaskCompletionChanged(item, completed), - ), + ...overdueEntries, if (widget.hasMoreOverdueTasks && widget.onLoadMoreOverdue != null) _AgendaLoadMoreRow( @@ -123,22 +139,12 @@ class _ScheduleAgendaViewState extends State { ), ], ), - if (noDateTasks.isNotEmpty) + if (noDateEntries.isNotEmpty || widget.hasMoreNoDateTasks) BusyMaxGroupedList( title: context.l10n.noDate, filled: true, children: [ - for (final item in noDateTasks) - _AgendaRow( - item: item, - onAnchorAvailable: widget.onItemAnchorAvailable, - onTap: (context, [globalPosition]) => - widget.onItemSelected(context, item, globalPosition), - onTaskCompletionChanged: item is TaskScheduleItem - ? (completed) => - widget.onTaskCompletionChanged(item, completed) - : null, - ), + ...noDateEntries, if (widget.hasMoreNoDateTasks && widget.onLoadMoreNoDate != null) _AgendaLoadMoreRow( @@ -148,28 +154,212 @@ class _ScheduleAgendaViewState extends State { ], ), for (final day in days) - BusyMaxGroupedList( - title: _dayLabel(context, day), - filled: true, - children: [ - for (final item in groups[day]!) - _AgendaRow( - item: item, - onAnchorAvailable: widget.onItemAnchorAvailable, - onTap: (context, [globalPosition]) => - widget.onItemSelected(context, item, globalPosition), - onTaskCompletionChanged: item is TaskScheduleItem - ? (completed) => - widget.onTaskCompletionChanged(item, completed) - : null, - ), - ], - ), + if (dayEntries[day]!.isNotEmpty) + BusyMaxGroupedList( + title: _dayLabel(context, day), + filled: true, + children: dayEntries[day]!, + ), ], ), ), ); } + + List _entriesFor( + List items, + _AgendaHierarchyIndex hierarchy, + Set emitted, + ) { + final entries = []; + final emittedDetachedParents = {}; + for (final item in items) { + if (item is! TaskScheduleItem) { + entries.add(_standaloneRow(item)); + continue; + } + + final key = _agendaTaskKey(item); + if (emitted.contains(key)) continue; + final parentKey = _agendaParentKey(item); + if (parentKey != null && hierarchy.tasks.containsKey(parentKey)) { + continue; + } + if (parentKey == null) { + entries.add(_taskGroup(item, hierarchy.children, emitted)); + continue; + } + if (!emittedDetachedParents.add(parentKey)) continue; + final detachedRoots = [ + for (final candidate in items.whereType()) + if (_agendaParentKey(candidate) == parentKey) candidate, + ]; + entries.add( + _detachedTaskGroup(detachedRoots, hierarchy.children, emitted), + ); + } + + for (final task in items.whereType()) { + if (!emitted.contains(_agendaTaskKey(task)) && hierarchy.isCyclic(task)) { + entries.add(_detachedTaskGroup([task], hierarchy.children, emitted)); + } + } + return entries; + } + + Widget _standaloneRow(ScheduleItem item) { + void select(BuildContext context, [Offset? globalPosition]) => + widget.onItemSelected(context, item, globalPosition); + final task = item is TaskScheduleItem ? item : null; + return _AgendaRow( + item: item, + onAnchorAvailable: widget.onItemAnchorAvailable, + onTap: select, + onTaskCompletionChanged: task == null + ? null + : (completed) => widget.onTaskCompletionChanged(task, completed), + ); + } + + Widget _taskGroup( + TaskScheduleItem root, + Map> children, + Set emitted, + ) { + emitted.add(_agendaTaskKey(root)); + return Column( + key: ValueKey( + 'agenda-task-group-${root.accountId}-${root.sourceId}-${root.id}', + ), + mainAxisSize: MainAxisSize.min, + children: [ + _standaloneRow(root), + ..._nestedRows(root, children, emitted, hierarchyRoot: root, depth: 1), + ], + ); + } + + Widget _detachedTaskGroup( + List roots, + Map> children, + Set emitted, + ) { + final visibleRoots = [ + for (final root in roots) + if (!emitted.contains(_agendaTaskKey(root))) root, + ]; + String? parentTitle; + for (final root in visibleRoots) { + final candidate = root.parentTitle?.trim(); + if (candidate != null && candidate.isNotEmpty) { + parentTitle = candidate; + break; + } + } + final nestedRows = []; + for (final root in visibleRoots) { + if (!emitted.add(_agendaTaskKey(root))) continue; + nestedRows.add( + _nestedTaskRow(root, hierarchyRoot: root, depth: 1, showSource: true), + ); + nestedRows.addAll( + _nestedRows(root, children, emitted, hierarchyRoot: root, depth: 2), + ); + } + final firstRoot = roots.first; + return Column( + key: ValueKey( + 'agenda-detached-task-group-${firstRoot.accountId}-${firstRoot.sourceId}-' + '${firstRoot.parentId ?? firstRoot.id}', + ), + mainAxisSize: MainAxisSize.min, + children: [ + _AgendaParentContext(title: parentTitle), + ...nestedRows, + ], + ); + } + + List _nestedRows( + TaskScheduleItem parent, + Map> children, + Set emitted, { + required TaskScheduleItem hierarchyRoot, + required int depth, + }) { + final rows = [ + for (final checklistItem in parent.checklistItems) + _nestedChecklistRow(parent, checklistItem, depth: depth), + ]; + for (final child in children[_agendaTaskKey(parent)] ?? const []) { + if (!emitted.add(_agendaTaskKey(child))) continue; + rows.add( + _nestedTaskRow(child, hierarchyRoot: hierarchyRoot, depth: depth), + ); + rows.addAll( + _nestedRows( + child, + children, + emitted, + hierarchyRoot: hierarchyRoot, + depth: depth + 1, + ), + ); + } + return rows; + } + + Widget _nestedTaskRow( + TaskScheduleItem task, { + required TaskScheduleItem hierarchyRoot, + required int depth, + bool showSource = false, + }) { + return _AgendaSubtaskRow( + key: ValueKey( + 'agenda-subtask-${task.accountId}-${task.sourceId}-${task.id}', + ), + depth: depth, + title: task.title, + completed: task.completed, + subtitleBuilder: (context) { + final values = [ + _nestedTaskScheduleLabel(context, task, hierarchyRoot), + if (showSource) ScheduleProjection.sourceLabelForScheduleItem(task), + ].where((value) => value.trim().isNotEmpty); + return values.join(' - '); + }, + onAnchorAvailable: widget.onItemAnchorAvailable == null + ? null + : (context) => widget.onItemAnchorAvailable!(task, context), + onTap: (context, [globalPosition]) => + widget.onItemSelected(context, task, globalPosition), + onCompletionChanged: (completed) => + widget.onTaskCompletionChanged(task, completed), + ); + } + + Widget _nestedChecklistRow( + TaskScheduleItem parent, + TaskChecklistItemEntity item, { + required int depth, + }) { + return _AgendaSubtaskRow( + key: ValueKey('agenda-checklist-${parent.id}-${item.id}'), + depth: depth, + title: item.title, + completed: item.completed, + onTap: (context, [globalPosition]) => + widget.onItemSelected(context, parent, globalPosition), + onCompletionChanged: widget.onChecklistItemCompletionChanged == null + ? null + : (completed) => widget.onChecklistItemCompletionChanged!( + parent, + item, + completed, + ), + ); + } } class _AgendaLoadMoreRow extends StatelessWidget { @@ -231,6 +421,123 @@ class _AgendaRow extends StatelessWidget { } } +class _AgendaParentContext extends StatelessWidget { + const _AgendaParentContext({required this.title}); + + final String? title; + + @override + Widget build(BuildContext context) { + final colors = BusyMaxSurfaceColors.of(context); + final label = title == null + ? context.l10n.subtasks + : '${context.l10n.parent}: $title'; + return Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + BusyMaxSpacing.md, + BusyMaxSpacing.sm, + BusyMaxSpacing.md, + BusyMaxSpacing.xs, + ), + child: Row( + children: [ + Icon( + Icons.account_tree_outlined, + size: BusyMaxSizes.iconSm, + color: colors.mutedForeground, + ), + const SizedBox(width: BusyMaxSpacing.sm), + Expanded( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colors.mutedForeground, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + +class _AgendaSubtaskRow extends StatelessWidget { + const _AgendaSubtaskRow({ + super.key, + required this.depth, + required this.title, + required this.completed, + required this.onTap, + this.subtitleBuilder, + this.onAnchorAvailable, + this.onCompletionChanged, + }); + + final int depth; + final String title; + final bool completed; + final ScheduleItemTapCallback onTap; + final String Function(BuildContext context)? subtitleBuilder; + final ValueChanged? onAnchorAvailable; + final ValueChanged? onCompletionChanged; + + @override + Widget build(BuildContext context) { + final onAnchorAvailable = this.onAnchorAvailable; + if (onAnchorAvailable != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) onAnchorAvailable(context); + }); + } + final colors = BusyMaxSurfaceColors.of(context); + final indentationLevel = depth.clamp(1, 12).toInt(); + final subtitle = subtitleBuilder?.call(context); + return Padding( + padding: EdgeInsetsDirectional.only( + start: BusyMaxSpacing.lg + (indentationLevel - 1) * BusyMaxSpacing.sm, + ), + child: DecoratedBox( + decoration: BoxDecoration( + border: BorderDirectional(start: BorderSide(color: colors.cardShade)), + ), + child: Padding( + padding: const EdgeInsetsDirectional.only(start: BusyMaxSpacing.xs), + child: BusyMaxActionRow( + title: title, + titleWidget: Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + decoration: completed ? TextDecoration.lineThrough : null, + color: completed + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: subtitle, + leading: Icon( + BusyMaxGlyphs.subdirectoryFor(Directionality.of(context)), + size: BusyMaxSizes.iconSm, + color: colors.mutedForeground, + ), + trailing: YaruCheckbox( + value: completed, + onChanged: onCompletionChanged == null + ? null + : (value) => onCompletionChanged!(value ?? false), + ), + onActivated: onTap, + ), + ), + ), + ); + } +} + class _AgendaItemMarker extends StatelessWidget { const _AgendaItemMarker({required this.item}); @@ -245,7 +552,14 @@ class _AgendaItemMarker extends StatelessWidget { item, Theme.of(context).colorScheme.brightness, ); - final icon = isTask ? YaruIcons.task_list : YaruIcons.calendar; + final task = item is TaskScheduleItem ? item as TaskScheduleItem : null; + final icon = task?.parentId != null + ? BusyMaxGlyphs.subdirectoryFor(Directionality.of(context)) + : task?.hasSubtasks == true + ? Icons.account_tree_outlined + : isTask + ? YaruIcons.task_list + : YaruIcons.calendar; return Icon(icon, size: BusyMaxSizes.iconSm, color: color); } } @@ -281,13 +595,16 @@ class _AgendaItemSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { + final task = item is TaskScheduleItem ? item as TaskScheduleItem : null; final values = [ scheduleTimeRange(context, item), + if (task?.parentTitle != null) + '${context.l10n.parent}: ${task!.parentTitle}', ScheduleProjection.sourceLabelForScheduleItem(item), ].where((value) => value.trim().isNotEmpty).join(' - '); return Text( values, - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: BusyMaxSurfaceColors.of(context).mutedForeground, @@ -306,3 +623,63 @@ String _dayLabel(BuildContext context, DateTime day) { final locale = Localizations.localeOf(context).toLanguageTag(); return DateFormat.yMMMMEEEEd(locale).format(day); } + +String _nestedTaskScheduleLabel( + BuildContext context, + TaskScheduleItem task, + TaskScheduleItem hierarchyRoot, +) { + final start = task.start; + final rootStart = hierarchyRoot.start; + if (start == null) { + return rootStart == null ? '' : context.l10n.noDate; + } + if (rootStart != null && DateUtils.isSameDay(start, rootStart)) { + return scheduleTimeRange(context, task); + } + final locale = Localizations.localeOf(context).toLanguageTag(); + final date = DateFormat.yMMMd(locale).format(start); + if (task.allDay) return date; + final time = scheduleTimeRange(context, task); + return time.trim().isEmpty ? date : '$date - $time'; +} + +String _agendaTaskKey(TaskScheduleItem task) => + '${task.accountId}\u0000${task.sourceId}\u0000${task.id}'; + +String? _agendaParentKey(TaskScheduleItem task) { + final parentId = task.parentId; + if (parentId == null || parentId.isEmpty) return null; + return '${task.accountId}\u0000${task.sourceId}\u0000$parentId'; +} + +class _AgendaHierarchyIndex { + _AgendaHierarchyIndex(List items) + : tasks = { + for (final task in items.whereType()) + _agendaTaskKey(task): task, + } { + for (final task in tasks.values) { + final parentKey = _agendaParentKey(task); + if (parentKey != null && tasks.containsKey(parentKey)) { + children.putIfAbsent(parentKey, () => []).add(task); + } + } + } + + final Map tasks; + final Map> children = {}; + + bool isCyclic(TaskScheduleItem task) { + final visited = {}; + TaskScheduleItem? current = task; + while (current != null) { + final key = _agendaTaskKey(current); + if (!visited.add(key)) return true; + final parentKey = _agendaParentKey(current); + if (parentKey == null) return false; + current = tasks[parentKey]; + } + return false; + } +} diff --git a/lib/src/features/schedule/presentation/schedule_item_exporter.dart b/lib/src/features/schedule/presentation/schedule_item_exporter.dart index 8e8ce1b..20d7e1d 100644 --- a/lib/src/features/schedule/presentation/schedule_item_exporter.dart +++ b/lib/src/features/schedule/presentation/schedule_item_exporter.dart @@ -4,9 +4,24 @@ import 'package:file_selector/file_selector.dart'; import '../../../schedule/schedule_item.dart'; -Future exportScheduleItemWithSaveDialog(ScheduleItem item) async { - final location = await getSaveLocation( +Future exportScheduleItemWithSaveDialog( + ScheduleItem item, { + String? rawICalendar, +}) { + return exportICalendarWithSaveDialog( suggestedName: scheduleExportFileName(item), + calendarData: + rawICalendar ?? + scheduleItemToICalendar(item, nowUtc: DateTime.now().toUtc()), + ); +} + +Future exportICalendarWithSaveDialog({ + required String suggestedName, + required String calendarData, +}) async { + final location = await getSaveLocation( + suggestedName: suggestedName, acceptedTypeGroups: const [ XTypeGroup( label: 'iCalendar', @@ -20,12 +35,16 @@ Future exportScheduleItemWithSaveDialog(ScheduleItem item) async { } final file = File(_ensureIcsExtension(location.path)); await file.parent.create(recursive: true); - await file.writeAsString( - scheduleItemToICalendar(item, nowUtc: DateTime.now().toUtc()), - ); + await file.writeAsString(calendarData); return file; } +String taskExportFileName({required String title, String? dueDate}) { + final date = DateTime.tryParse(dueDate ?? ''); + return 'busymax-task-${date == null ? 'no-date' : _formatDate(date)}-' + '${_sanitizeFilePart(title)}.ics'; +} + String scheduleExportFileName(ScheduleItem item) { final prefix = item is CalendarScheduleItem ? 'event' : 'task'; final title = _sanitizeFilePart(item.title); diff --git a/lib/src/features/schedule/presentation/schedule_sidebar.dart b/lib/src/features/schedule/presentation/schedule_sidebar.dart index fbf0b50..88a8856 100644 --- a/lib/src/features/schedule/presentation/schedule_sidebar.dart +++ b/lib/src/features/schedule/presentation/schedule_sidebar.dart @@ -18,7 +18,8 @@ import '../../accounts/data/accounts_repository.dart'; import '../../calendar/data/calendar_repository.dart'; import '../../sync/sync_auth_error.dart'; import '../../task_lists/data/task_lists_repository.dart'; -import '../../../task_providers/task_provider.dart'; +import '../../tasks/domain/task_capabilities.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'mini_calendar.dart'; class ScheduleSidebar extends ConsumerWidget { @@ -76,12 +77,14 @@ class ScheduleSidebar extends ConsumerWidget { } class _SourceRow extends ConsumerWidget { - const _SourceRow({super.key, required this.source}); + const _SourceRow({super.key, required this.account, required this.source}); + final AccountEntity account; final CalendarSourceEntity source; @override Widget build(BuildContext context, WidgetRef ref) { + final providerWebUri = scheduleCalendarProviderWebUri(account, source); return _CompactSourceRow( title: source.summary, leading: _SourceDot( @@ -110,7 +113,9 @@ class _SourceRow extends ConsumerWidget { case 'refresh': unawaited(_refreshCalendarSource(context, ref, source)); case 'open': - unawaited(_openProviderWeb(_calendarWebUri(source))); + if (providerWebUri != null) { + unawaited(_openProviderWeb(providerWebUri)); + } case 'rename': unawaited(_renameCalendar(context, ref, source)); case 'delete': @@ -123,11 +128,12 @@ class _SourceRow extends ConsumerWidget { label: context.l10n.refreshCalendar, icon: YaruIcons.refresh, ), - BusyMaxMenuEntry( - value: 'open', - label: context.l10n.openInProvider, - icon: Icons.open_in_browser_outlined, - ), + if (providerWebUri != null) + BusyMaxMenuEntry( + value: 'open', + label: context.l10n.openInProvider, + icon: Icons.open_in_browser_outlined, + ), BusyMaxMenuEntry( value: 'rename', label: context.l10n.rename, @@ -431,6 +437,7 @@ class _AccountCalendarSources extends ConsumerWidget { source.accountId, source.id, )), + account: account, source: source, ), ], @@ -477,11 +484,36 @@ class _TaskListScheduleRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(appSettingsControllerProvider); + final providerWebUri = scheduleTaskProviderWebUri(account); final visible = settings.isTaskListVisibleInSchedule( list.accountId, list.id, ); - final title = _taskListLabel(context, account, list); + final title = scheduleTaskListLabel(context, account, list); + final davCapabilities = list.davCollectionId == null + ? null + : ref + .watch( + davTaskCollectionCapabilitiesProvider(( + accountId: list.accountId, + taskListId: list.id, + )), + ) + .valueOrNull; + final canRename = _canRenameTaskList(account, list, davCapabilities); + final canDelete = _canDeleteTaskList(account, list, davCapabilities); + final renameRestriction = _taskListRenameRestriction( + context, + account, + list, + canRename, + ); + final deleteRestriction = _taskListDeleteRestriction( + context, + account, + list, + canDelete, + ); return _CompactSourceRow( title: title, leading: _SourceDot( @@ -509,7 +541,9 @@ class _TaskListScheduleRow extends ConsumerWidget { case 'refresh': unawaited(_refreshTaskListAccount(context, ref, account.id)); case 'open': - unawaited(_openProviderWeb(_taskProviderWebUri(account))); + if (providerWebUri != null) { + unawaited(_openProviderWeb(providerWebUri)); + } case 'rename': unawaited(_renameTaskList(context, ref, list)); case 'delete': @@ -522,28 +556,29 @@ class _TaskListScheduleRow extends ConsumerWidget { label: context.l10n.refreshList, icon: YaruIcons.refresh, ), - BusyMaxMenuEntry( - value: 'open', - label: context.l10n.openInProvider, - icon: Icons.open_in_browser_outlined, - ), + if (providerWebUri != null) + BusyMaxMenuEntry( + value: 'open', + label: context.l10n.openInProvider, + icon: Icons.open_in_browser_outlined, + ), BusyMaxMenuEntry( value: 'rename', label: context.l10n.rename, icon: Icons.edit_outlined, - enabled: _canRenameOrDeleteTaskList(account, list), - tooltip: _canRenameOrDeleteTaskList(account, list) - ? null - : context.l10n.builtInMicrosoftListCannotRenameDelete, + enabled: canRename, + tooltip: renameRestriction, ), BusyMaxMenuEntry( value: 'delete', - label: context.l10n.delete, + label: + account.provider == BusyProvider.nextcloud && + list.isShared == true + ? context.l10n.unshare + : context.l10n.delete, icon: YaruIcons.trash, - enabled: _canRenameOrDeleteTaskList(account, list), - tooltip: _canRenameOrDeleteTaskList(account, list) - ? null - : context.l10n.builtInMicrosoftListCannotRenameDelete, + enabled: canDelete, + tooltip: deleteRestriction, destructive: true, ), ], @@ -553,14 +588,18 @@ class _TaskListScheduleRow extends ConsumerWidget { } } -String _taskListLabel( +@visibleForTesting +String scheduleTaskListLabel( BuildContext context, AccountEntity account, TaskListEntity list, ) { - final provider = account.provider == BusyProvider.google - ? context.l10n.googleTasksProvider - : context.l10n.microsoftTodoProvider; + final provider = switch (account.provider) { + BusyProvider.google => context.l10n.googleTasksProvider, + BusyProvider.microsoft => context.l10n.microsoftTodoProvider, + BusyProvider.appleICloud => context.l10n.appleICloudTasksProvider, + BusyProvider.nextcloud => context.l10n.nextcloudTasksProvider, + }; final title = list.title.trim(); if (title.isEmpty || title.toLowerCase() == provider.toLowerCase() || @@ -579,20 +618,48 @@ Color? _colorFromHex(String? value) { return parsed == null ? null : Color(0xff000000 | parsed); } -Uri _calendarWebUri(CalendarSourceEntity source) { - if (source.provider == TaskProvider.google) { - return Uri.https('calendar.google.com', '/calendar/u/0/r', { - 'cid': source.providerCalendarId, - }); +@visibleForTesting +Uri? scheduleCalendarProviderWebUri( + AccountEntity account, + CalendarSourceEntity source, +) { + if (source.accountId != account.id || source.provider != account.provider) { + return null; } - return Uri.https('outlook.live.com', '/calendar/0/view/month'); + return switch (account.provider) { + BusyProvider.google => Uri.https('calendar.google.com', '/calendar/u/0/r', { + 'cid': source.providerCalendarId, + }), + BusyProvider.microsoft => Uri.https( + 'outlook.live.com', + '/calendar/0/view/month', + ), + BusyProvider.appleICloud => null, + BusyProvider.nextcloud => _safeAccountWebUri(account.authority), + }; } -Uri _taskProviderWebUri(AccountEntity account) { - if (account.provider == TaskProvider.google) { - return Uri.https('tasks.google.com', '/'); +@visibleForTesting +Uri? scheduleTaskProviderWebUri(AccountEntity account) { + return switch (account.provider) { + BusyProvider.google => Uri.https('tasks.google.com', '/'), + BusyProvider.microsoft => Uri.https('to-do.office.com', '/tasks/'), + BusyProvider.appleICloud => null, + BusyProvider.nextcloud => _safeAccountWebUri(account.authority), + }; +} + +Uri? _safeAccountWebUri(String authority) { + final uri = Uri.tryParse(authority.trim()); + if (uri == null || + uri.scheme.toLowerCase() != 'https' || + uri.host.isEmpty || + uri.userInfo.isNotEmpty || + uri.hasQuery || + uri.hasFragment) { + return null; } - return Uri.https('to-do.office.com', '/tasks/'); + return uri; } Future _openProviderWeb(Uri uri) async { @@ -656,13 +723,62 @@ Future _handleRefreshFailure( ); } -bool _canRenameOrDeleteTaskList(AccountEntity account, TaskListEntity list) { - if (account.provider == TaskProvider.microsoft) { +bool _canRenameTaskList( + AccountEntity account, + TaskListEntity list, + TaskCollectionCapabilities? davCapabilities, +) { + if (account.provider == BusyProvider.microsoft) { return list.canRenameOrDeleteForMicrosoft; } + if (list.davCollectionId != null) { + return davCapabilities?.supportsListRename ?? false; + } return true; } +bool _canDeleteTaskList( + AccountEntity account, + TaskListEntity list, + TaskCollectionCapabilities? davCapabilities, +) { + if (account.provider == BusyProvider.microsoft) { + return list.canRenameOrDeleteForMicrosoft; + } + if (list.davCollectionId != null) { + return davCapabilities?.supportsListDelete ?? false; + } + return true; +} + +String? _taskListRenameRestriction( + BuildContext context, + AccountEntity account, + TaskListEntity list, + bool enabled, +) { + if (enabled) return null; + if (account.provider == BusyProvider.microsoft && + !list.canRenameOrDeleteForMicrosoft) { + return context.l10n.builtInMicrosoftListCannotRenameDelete; + } + return context.l10n.readOnlyTaskListCannotRename; +} + +String? _taskListDeleteRestriction( + BuildContext context, + AccountEntity account, + TaskListEntity list, + bool enabled, +) { + if (enabled) return null; + if (account.provider == BusyProvider.microsoft && + !list.canRenameOrDeleteForMicrosoft) { + return context.l10n.builtInMicrosoftListCannotRenameDelete; + } + return context.l10n.taskListCannotDelete; +} + Future _renameCalendar( BuildContext context, WidgetRef ref, @@ -725,9 +841,21 @@ Future _renameTaskList( title.trim() == list.title) { return; } - await ref - .read(taskListsRepositoryForAccountProvider(list.accountId)) - .renameTaskList(list.id, title.trim()); + try { + await ref + .read(taskListsRepositoryForAccountProvider(list.accountId)) + .renameTaskList(list.id, title.trim()); + } on Object catch (error) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.taskListRenameFailed(syncFailureMessage(error)), + ), + ), + ); + } + } } Future _deleteTaskList( @@ -737,16 +865,34 @@ Future _deleteTaskList( ) async { final confirmed = await showBusyMaxConfirm( context, - title: context.l10n.deleteList, - message: context.l10n.deleteListConfirmation(list.title), - confirmLabel: context.l10n.delete, + title: list.isShared == true + ? context.l10n.unshare + : context.l10n.deleteList, + message: list.isShared == true + ? context.l10n.unshareTaskListConfirmation(list.title) + : context.l10n.deleteTaskListConfirmation(list.title), + confirmLabel: list.isShared == true + ? context.l10n.unshare + : context.l10n.delete, destructive: true, headerBarService: ref.read(linuxHeaderBarServiceProvider), ); if (!confirmed) { return; } - await ref - .read(taskListsRepositoryForAccountProvider(list.accountId)) - .deleteTaskList(list.id); + try { + await ref + .read(taskListsRepositoryForAccountProvider(list.accountId)) + .deleteTaskList(list.id); + } on Object catch (error) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.taskListDeleteFailed(syncFailureMessage(error)), + ), + ), + ); + } + } } diff --git a/lib/src/features/schedule/presentation/schedule_task_chip.dart b/lib/src/features/schedule/presentation/schedule_task_chip.dart index bfd0007..ad7a9e1 100644 --- a/lib/src/features/schedule/presentation/schedule_task_chip.dart +++ b/lib/src/features/schedule/presentation/schedule_task_chip.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:yaru/yaru.dart'; import '../../../app/busymax_design.dart'; +import '../../../app/busymax_glyphs.dart'; import '../../../app/busymax_surface_colors.dart'; +import '../../../l10n/l10n.dart'; import '../../../schedule/schedule_item.dart'; import '../../../schedule/schedule_projection.dart'; import 'schedule_event_block.dart'; @@ -41,6 +43,9 @@ class ScheduleTaskChip extends StatelessWidget { scheduleTimeRange(context, item), ScheduleProjection.sourceLabelForScheduleItem(item), ].join(' · '); + final hierarchyDetails = item.parentTitle == null + ? null + : '${context.l10n.parent}: ${item.parentTitle}'; final blockWidth = scheduleSafeBlockWidth(width); final blockHeight = scheduleSafeBlockHeight(height); final horizontalPadding = compact ? 5.0 : 8.0; @@ -53,7 +58,11 @@ class ScheduleTaskChip extends StatelessWidget { Offset? pointerDownPosition; return Tooltip( - message: '${item.title}\n$details', + message: [ + item.title, + if (hierarchyDetails != null) hierarchyDetails, + details, + ].join('\n'), waitDuration: const Duration(milliseconds: 600), child: SizedBox( width: blockWidth, @@ -92,6 +101,19 @@ class ScheduleTaskChip extends StatelessWidget { child: showContent ? Row( children: [ + if ((item.parentId != null || item.hasSubtasks) && + contentWidth >= 20) ...[ + Icon( + item.parentId != null + ? BusyMaxGlyphs.subdirectoryFor( + Directionality.of(context), + ) + : Icons.account_tree_outlined, + size: compact ? 12 : BusyMaxSizes.iconSm, + color: surfaceColors.mutedForeground, + ), + const SizedBox(width: BusyMaxSpacing.xs), + ], if (showCheckbox) ...[ SizedBox.square( dimension: checkboxSize, diff --git a/lib/src/features/schedule/presentation/schedule_workspace.dart b/lib/src/features/schedule/presentation/schedule_workspace.dart index 1ac1454..4703fd1 100644 --- a/lib/src/features/schedule/presentation/schedule_workspace.dart +++ b/lib/src/features/schedule/presentation/schedule_workspace.dart @@ -32,11 +32,12 @@ import '../../../schedule/schedule_repository.dart'; import '../../../schedule/schedule_scope.dart'; import '../../../schedule/schedule_source_visibility.dart'; import '../../../schedule/schedule_view_mode.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../../calendar/presentation/event_editor.dart'; import '../../calendar/presentation/event_editor_draft.dart'; import '../../task_lists/data/task_lists_repository.dart'; import '../../tasks/data/tasks_repository.dart'; +import '../../tasks/domain/task_checklist_item.dart'; import '../../tasks/presentation/new_task_dialog.dart'; import '../../tasks/presentation/task_details_pane.dart'; import 'schedule_agenda_view.dart'; @@ -558,6 +559,8 @@ class _ScheduleWorkspaceState extends ConsumerState { ), onItemAnchorAvailable: _handleItemAnchorAvailable, onTaskCompletionChanged: _setTaskCompleted, + onChecklistItemCompletionChanged: + _setChecklistItemCompleted, canCreateEvent: writableSources.isNotEmpty, canCreateTask: canCreateTask, searchActive: searchHasQuery, @@ -596,6 +599,9 @@ class _ScheduleWorkspaceState extends ConsumerState { onDirtyChanged: (dirty) { _taskDetailsDirty = dirty; }, + onMutationCommitted: () { + if (mounted) setState(() {}); + }, child: body, ); }, @@ -951,8 +957,17 @@ class _ScheduleWorkspaceState extends ConsumerState { final datedItems = await currentItems; final overduePage = await overdueTasks; final noDatePage = await noDateTasks; + final items = await repository.includeTaskAncestors( + [...datedItems, ...overduePage.items, ...noDatePage.items], + filters: ScheduleFilters( + accountIds: accountIds, + taskListKeys: taskListKeys, + taskListFilterActive: true, + includeTasks: _scope != ScheduleScope.events, + ), + ); return _ScheduleItemsResult( - items: [...datedItems, ...overduePage.items, ...noDatePage.items], + items: items, hasMoreOverdueTasks: overduePage.hasMore, hasMoreNoDateTasks: noDatePage.hasMore, ); @@ -1560,7 +1575,18 @@ class _ScheduleWorkspaceState extends ConsumerState { Future _exportItem(ScheduleItem item) async { try { - final file = await exportScheduleItemWithSaveDialog(item); + String? rawICalendar; + if (item is TaskScheduleItem && + (item.provider == BusyProvider.nextcloud || + item.provider == BusyProvider.appleICloud)) { + rawICalendar = await ref + .read(tasksRepositoryForAccountProvider(item.accountId)) + .nativeTaskExport(item.sourceId, item.id); + } + final file = await exportScheduleItemWithSaveDialog( + item, + rawICalendar: rawICalendar, + ); if (file == null) { return; } @@ -1900,6 +1926,7 @@ class _ScheduleWorkspaceState extends ConsumerState { context, initialDraft: draft, sources: editableSources, + accounts: _latestAccounts, categorySuggestionsByAccount: _categorySuggestionsByAccount(), headerBarService: ref.read(linuxHeaderBarServiceProvider), ); @@ -1908,7 +1935,7 @@ class _ScheduleWorkspaceState extends ConsumerState { } final deletedEventId = result.deletedEventId; if (deletedEventId != null) { - await _deleteEvent(deletedEventId); + await _deleteEvent(deletedEventId, recurringScope: result.deletionScope); return; } final savedDraft = result.draft; @@ -1917,10 +1944,13 @@ class _ScheduleWorkspaceState extends ConsumerState { } } - Future _deleteEvent(String eventId) async { + Future _deleteEvent( + String eventId, { + RecurringEventMutationScope? recurringScope, + }) async { final accountId = await ref .read(calendarRepositoryProvider) - .deleteLocalEvent(eventId); + .deleteLocalEvent(eventId, recurringScope: recurringScope); _requestCalendarMutationSync(accountId); if (mounted) { setState(() {}); @@ -1931,6 +1961,14 @@ class _ScheduleWorkspaceState extends ConsumerState { if (!item.capabilities.canDelete) { return; } + RecurringEventMutationScope? recurringScope; + if (item is CalendarScheduleItem && + item.providerRecurringEventId != null && + (item.provider == BusyProvider.appleICloud || + item.provider == BusyProvider.nextcloud)) { + recurringScope = await _chooseRecurringEventMutationScope(); + if (recurringScope == null || !mounted) return; + } final confirmed = await showBusyMaxConfirm( context, title: item is CalendarScheduleItem @@ -1947,7 +1985,7 @@ class _ScheduleWorkspaceState extends ConsumerState { return; } if (item is CalendarScheduleItem) { - await _deleteEvent(item.id); + await _deleteEvent(item.id, recurringScope: recurringScope); return; } if (item is TaskScheduleItem) { @@ -1960,6 +1998,48 @@ class _ScheduleWorkspaceState extends ConsumerState { } } + Future _chooseRecurringEventMutationScope() { + return showBusyMaxModalEditorDialog( + context, + headerBarService: ref.read(linuxHeaderBarServiceProvider), + maxHeight: 420, + builder: (dialogContext) => BusyMaxModalEditorScaffold( + title: context.l10n.recurringEventScope, + cancelLabel: context.l10n.cancel, + saveLabel: context.l10n.delete, + onCancel: () => Navigator.of(dialogContext).pop(), + onSave: null, + children: [ + BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxActionRow( + title: context.l10n.entireSeries, + subtitle: context.l10n.chooseRecurringEventScope, + leading: const Icon(Icons.repeat), + onTap: () => Navigator.of( + dialogContext, + ).pop(RecurringEventMutationScope.entireSeries), + ), + BusyMaxActionRow( + title: context.l10n.singleOccurrence, + leading: const Icon(Icons.event_outlined), + onTap: () => Navigator.of( + dialogContext, + ).pop(RecurringEventMutationScope.singleOccurrence), + ), + BusyMaxActionRow( + title: context.l10n.thisAndFutureUnavailable, + leading: const Icon(Icons.update_disabled_outlined), + enabled: false, + ), + ], + ), + ], + ), + ); + } + Future _openNewTask( List accounts, { DateTime? due, @@ -2048,6 +2128,29 @@ class _ScheduleWorkspaceState extends ConsumerState { } } + Future _setChecklistItemCompleted( + TaskScheduleItem parent, + TaskChecklistItemEntity item, + bool completed, + ) async { + try { + await ref + .read(tasksRepositoryForAccountProvider(parent.accountId)) + .patchChecklistSubtask( + taskListId: parent.sourceId, + parentTaskId: parent.id, + checklistItemId: item.id, + completed: completed, + ); + if (mounted) setState(() {}); + } on Object catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.syncFailed(error.toString()))), + ); + } + } + Future _refreshAll() async { try { final accounts = await ref @@ -2195,7 +2298,7 @@ Object? _eventRemindersForEdit(BusyProvider provider, List minutes) { if (normalized.isEmpty) { return null; } - if (provider == TaskProvider.google) { + if (provider != BusyProvider.microsoft) { return { 'useDefault': false, 'overrides': [ @@ -2268,6 +2371,7 @@ class _ScheduleBody extends StatelessWidget { required this.onItemSelected, required this.onItemAnchorAvailable, required this.onTaskCompletionChanged, + required this.onChecklistItemCompletionChanged, required this.canCreateEvent, required this.canCreateTask, required this.searchActive, @@ -2306,6 +2410,12 @@ class _ScheduleBody extends StatelessWidget { final ScheduleItemAnchorCallback onItemAnchorAvailable; final void Function(TaskScheduleItem item, bool completed) onTaskCompletionChanged; + final void Function( + TaskScheduleItem parent, + TaskChecklistItemEntity item, + bool completed, + ) + onChecklistItemCompletionChanged; final bool canCreateEvent; final bool canCreateTask; final bool searchActive; @@ -2399,6 +2509,7 @@ class _ScheduleBody extends StatelessWidget { onItemSelected: onItemSelected, onItemAnchorAvailable: onItemAnchorAvailable, onTaskCompletionChanged: onTaskCompletionChanged, + onChecklistItemCompletionChanged: onChecklistItemCompletionChanged, ), }; } @@ -2441,12 +2552,14 @@ class _ScheduleTaskDetailsOverlay extends StatefulWidget { required this.target, required this.onClose, required this.onDirtyChanged, + required this.onMutationCommitted, }); final Widget child; final _TaskDetailsTarget? target; final VoidCallback onClose; final ValueChanged onDirtyChanged; + final VoidCallback onMutationCommitted; @override State<_ScheduleTaskDetailsOverlay> createState() => @@ -2547,6 +2660,8 @@ class _ScheduleTaskDetailsOverlayState taskId: target.taskId, onClose: widget.onClose, onDirtyChanged: widget.onDirtyChanged, + onTaskMutationCommitted: (_) => + widget.onMutationCommitted(), dialogBarrierColor: Colors.transparent, ), ), diff --git a/lib/src/features/settings/presentation/settings_screen.dart b/lib/src/features/settings/presentation/settings_screen.dart index fb30170..0d02106 100644 --- a/lib/src/features/settings/presentation/settings_screen.dart +++ b/lib/src/features/settings/presentation/settings_screen.dart @@ -16,12 +16,18 @@ import '../../../app/busymax_glyphs.dart'; import '../../../app/busymax_keyboard_shortcuts_dialog.dart'; import '../../../app/busymax_layout.dart'; import '../../../core/logging/redacting_logger.dart'; -import '../../../google_tasks/oauth/oauth_models.dart'; +import '../../../dav/auth/dav_account_dialogs.dart'; +import '../../../dav/dav_errors.dart'; +import '../../../dav/http/dav_http_transport.dart'; +import '../../../dav/mutation/dav_conflict_repository.dart'; +import '../../../dav/storage/dav_settings_repository.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import '../../../l10n/app_locale.dart'; import '../../../l10n/l10n.dart'; import '../../../platform/linux_header_bar_service.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../../accounts/data/accounts_repository.dart'; +import '../../accounts/domain/account_connection_state.dart'; import '../../auth/data/auth_repository.dart'; import '../../diagnostics/presentation/diagnostics_screen.dart'; import '../../feedback/presentation/feedback_dialog.dart'; @@ -47,7 +53,8 @@ class _SettingsScreenState extends ConsumerState { StreamSubscription? _headerBarActions; var _headerBarReady = false; var _nativeHeaderBarAvailable = false; - TaskProvider? _connectingProvider; + BusyProvider? _connectingProvider; + DavCancellationToken? _davCancellation; final _removingAccountIds = {}; @override @@ -62,6 +69,7 @@ class _SettingsScreenState extends ConsumerState { @override void dispose() { + _davCancellation?.cancel(); _headerBarSession.dispose(); unawaited(_headerBarActions?.cancel()); super.dispose(); @@ -80,6 +88,10 @@ class _SettingsScreenState extends ConsumerState { final selectedAccount = ref.watch(selectedAccountProvider); final accounts = ref.watch(accountManagementStreamProvider).valueOrNull ?? const []; + final davCollections = + ref.watch(davCollectionsStreamProvider).valueOrNull ?? const []; + final davConflicts = + ref.watch(davConflictsStreamProvider).valueOrNull ?? const []; final config = ref.watch(buildConfigProvider); final settings = ref.watch(appSettingsControllerProvider); final settingsController = ref.read(appSettingsControllerProvider.notifier); @@ -93,15 +105,36 @@ class _SettingsScreenState extends ConsumerState { googleConfigured: config.hasGoogleOAuthClientId, microsoftConfigured: config.hasMicrosoftOAuthClientId, connectingProvider: _connectingProvider, - onAddGoogle: () => unawaited(_connectAccount(TaskProvider.google)), + onAddGoogle: () => unawaited(_connectAccount(BusyProvider.google)), onAddMicrosoft: () => - unawaited(_connectAccount(TaskProvider.microsoft)), - onReconnect: (account) => unawaited(_connectAccount(account.provider)), + unawaited(_connectAccount(BusyProvider.microsoft)), + onAddApple: () => unawaited(_connectAccount(BusyProvider.appleICloud)), + onAddNextcloud: () => + unawaited(_connectAccount(BusyProvider.nextcloud)), + onCancelConnection: _cancelAccountConnection, + onReconnect: (account) => + unawaited(_connectAccount(account.provider, reconnecting: account)), onCreateTaskList: (accountId) => _createTaskList(context, ref, accountId), removingAccountIds: _removingAccountIds, onRemoveAccount: (account) => unawaited(_removeAccount(context, ref, account)), + davCollections: davCollections, + davConflicts: davConflicts, + onRefreshCollections: (account) => + unawaited(_refreshCollections(account)), + onEventsSelected: (collection, selected) => unawaited( + ref + .read(davSettingsRepositoryProvider) + .setEventsSelected(collection.id, selected), + ), + onTasksSelected: (collection, selected) => unawaited( + ref + .read(davSettingsRepositoryProvider) + .setTasksSelected(collection.id, selected), + ), + onResolveConflict: (conflict, resolution) => + unawaited(_resolveConflict(conflict, resolution)), ), SettingsPage.schedule => BusyMaxGroupedList( title: l10n.scheduleDisplaySettings, @@ -474,24 +507,76 @@ class _SettingsScreenState extends ConsumerState { }); } - Future _connectAccount(TaskProvider provider) async { + Future _connectAccount( + BusyProvider provider, { + AccountEntity? reconnecting, + }) async { if (_connectingProvider != null) { return; } + AppleICloudCredentialInput? appleInput; + String? nextcloudServer; + if (provider == BusyProvider.appleICloud) { + appleInput = await showAppleICloudCredentialDialog( + context, + fixedEmail: reconnecting?.email ?? reconnecting?.providerAccountId, + headerBarService: ref.read(linuxHeaderBarServiceProvider), + ); + if (appleInput == null || !mounted) return; + } else if (provider == BusyProvider.nextcloud) { + nextcloudServer = await showNextcloudServerDialog( + context, + initialServer: reconnecting?.authority, + headerBarService: ref.read(linuxHeaderBarServiceProvider), + ); + if (nextcloudServer == null || !mounted) return; + } final repository = ref.read(authRepositoryProvider); final runSync = ref.read(signedInSyncRunnerProvider); setState(() => _connectingProvider = provider); try { - final signedIn = switch (provider) { - TaskProvider.google => await repository.signIn(), - TaskProvider.microsoft => await repository.signInWithMicrosoft(), - }; - final accountId = signedIn.accountId; + String? accountId; + switch (provider) { + case BusyProvider.google: + accountId = (await repository.signIn()).accountId; + case BusyProvider.microsoft: + accountId = (await repository.signInWithMicrosoft()).accountId; + case BusyProvider.appleICloud: + final cancellation = DavCancellationToken(); + _davCancellation = cancellation; + final onboarding = ref.read(davAccountOnboardingServiceProvider); + accountId = reconnecting == null + ? (await onboarding.connectAppleICloud( + email: appleInput!.email, + appSpecificPassword: appleInput.password, + cancellationToken: cancellation, + )).accountId + : (await onboarding.replaceAppleAppSpecificPassword( + accountId: reconnecting.id, + appSpecificPassword: appleInput!.password, + cancellationToken: cancellation, + )).accountId; + case BusyProvider.nextcloud: + final cancellation = DavCancellationToken(); + _davCancellation = cancellation; + final onboarding = ref.read(davAccountOnboardingServiceProvider); + accountId = reconnecting == null + ? (await onboarding.connectNextcloud( + enteredServer: nextcloudServer!, + cancellationToken: cancellation, + )).accountId + : (await onboarding.reconnectNextcloud( + accountId: reconnecting.id, + enteredServer: nextcloudServer!, + cancellationToken: cancellation, + )).accountId; + } if (accountId != null) { unawaited(_syncConnectedAccount(runSync, accountId)); } } on Object catch (error) { - if (error is OAuthException && error.code == 'OAuthSignInCancelled') { + if ((error is OAuthException && error.code == 'OAuthSignInCancelled') || + (error is DavException && error.kind == DavErrorKind.cancelled)) { return; } if (mounted) { @@ -499,11 +584,19 @@ class _SettingsScreenState extends ConsumerState { } } finally { if (mounted) { - setState(() => _connectingProvider = null); + setState(() { + _connectingProvider = null; + _davCancellation = null; + }); } } } + void _cancelAccountConnection() { + _davCancellation?.cancel(); + ref.read(davAccountOnboardingServiceProvider).cancelNextcloudLogin(); + } + Future _syncConnectedAccount( SignedInSyncRunner runSync, String accountId, @@ -533,7 +626,7 @@ class _SettingsScreenState extends ConsumerState { context, accountLabel: account.displayLabel, canRevokeGoogleAuthorization: - account.provider == TaskProvider.google && account.isSignedIn, + account.provider == BusyProvider.google && account.isSignedIn, headerBarService: ref.read(linuxHeaderBarServiceProvider), ); if (!context.mounted || options == null) { @@ -542,15 +635,31 @@ class _SettingsScreenState extends ConsumerState { setState(() => _removingAccountIds.add(account.id)); try { - final result = await ref - .read(authRepositoryProvider) - .removeAccount( - accountId: account.id, - revokeAuthorization: options.revokeGoogleAuthorization, - ); + final dav = + account.provider == BusyProvider.appleICloud || + account.provider == BusyProvider.nextcloud; + final result = dav + ? null + : await ref + .read(authRepositoryProvider) + .removeAccount( + accountId: account.id, + revokeAuthorization: options.revokeGoogleAuthorization, + ); + final davResult = dav + ? await ref + .read(davAccountOnboardingServiceProvider) + .removeAccount(account.id) + : null; if (context.mounted) { - if (result.authorizationRevocationFailed) { + if (result?.authorizationRevocationFailed ?? false) { _showMessage(context, context.l10n.accountRemovedGoogleRevokeFailed); + } else if (davResult?.remoteRevocationAttempted == true && + !davResult!.remoteRevocationSucceeded) { + _showMessage( + context, + context.l10n.nextcloudAccountRemovedRevokeFailed, + ); } await _afterAccountRemoved(context, ref, account.id); } @@ -579,9 +688,19 @@ class _SettingsScreenState extends ConsumerState { return; } - await ref - .read(taskListsRepositoryForAccountProvider(accountId)) - .createTaskList(title.trim()); + try { + await ref + .read(taskListsRepositoryForAccountProvider(accountId)) + .createTaskList(title.trim()); + } on Object catch (error) { + _settingsLogger.warning('Task-list creation failed: $error'); + if (context.mounted) { + _showMessage( + context, + context.l10n.taskListCreateFailed(syncFailureMessage(error)), + ); + } + } } Future _fullSync( @@ -611,6 +730,39 @@ class _SettingsScreenState extends ConsumerState { } } + Future _refreshCollections(AccountEntity account) async { + try { + await ref.read(signedInSyncRunnerProvider)(account.id, true); + if (mounted) _showMessage(context, context.l10n.syncComplete); + } on Object catch (error) { + if (mounted) { + _showMessage( + context, + context.l10n.syncFailed(syncFailureMessage(error)), + ); + } + } + } + + Future _resolveConflict( + DavConflictEntity conflict, + DavConflictResolution resolution, + ) async { + try { + await ref + .read(davConflictResolutionServiceProvider) + .resolve(conflict.id, resolution); + await ref + .read(accountSyncOperationsProvider) + .syncAccount(conflict.accountId, full: false); + } on Object catch (error) { + _settingsLogger.warning('DAV conflict resolution failed: $error'); + if (mounted) { + _showMessage(context, context.l10n.conflictResolutionFailed); + } + } + } + void _showMessage(BuildContext context, String message) { ScaffoldMessenger.of( context, @@ -826,22 +978,46 @@ class _AccountManagementSection extends StatelessWidget { required this.connectingProvider, required this.onAddGoogle, required this.onAddMicrosoft, + required this.onAddApple, + required this.onAddNextcloud, + required this.onCancelConnection, required this.onReconnect, required this.onCreateTaskList, required this.removingAccountIds, required this.onRemoveAccount, + required this.davCollections, + required this.davConflicts, + required this.onRefreshCollections, + required this.onEventsSelected, + required this.onTasksSelected, + required this.onResolveConflict, }); final List accounts; final bool googleConfigured; final bool microsoftConfigured; - final TaskProvider? connectingProvider; + final BusyProvider? connectingProvider; final VoidCallback onAddGoogle; final VoidCallback onAddMicrosoft; + final VoidCallback onAddApple; + final VoidCallback onAddNextcloud; + final VoidCallback onCancelConnection; final void Function(AccountEntity account) onReconnect; final void Function(String accountId) onCreateTaskList; final Set removingAccountIds; final void Function(AccountEntity account) onRemoveAccount; + final List davCollections; + final List davConflicts; + final void Function(AccountEntity account) onRefreshCollections; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onEventsSelected; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onTasksSelected; + final void Function( + DavConflictEntity conflict, + DavConflictResolution resolution, + ) + onResolveConflict; @override Widget build(BuildContext context) { @@ -854,21 +1030,41 @@ class _AccountManagementSection extends StatelessWidget { title: l10n.account, filled: true, children: [ - if (googleConfigured) - BusyMaxActionRow( - title: connectingProvider == TaskProvider.google - ? l10n.waitingForGoogleSignIn - : l10n.addGoogleAccount, - leading: const Icon(YaruIcons.plus), - onTap: connecting ? null : onAddGoogle, - ), - if (microsoftConfigured) + BusyMaxActionRow( + title: connectingProvider == BusyProvider.google + ? l10n.waitingForGoogleSignIn + : l10n.addGoogleAccount, + subtitle: googleConfigured ? null : l10n.providerNotConfigured, + leading: const Icon(YaruIcons.plus), + onTap: connecting || !googleConfigured ? null : onAddGoogle, + ), + BusyMaxActionRow( + title: connectingProvider == BusyProvider.microsoft + ? l10n.waitingForMicrosoftSignIn + : l10n.addMicrosoftAccount, + subtitle: microsoftConfigured ? null : l10n.providerNotConfigured, + leading: const Icon(YaruIcons.plus), + onTap: connecting || !microsoftConfigured ? null : onAddMicrosoft, + ), + BusyMaxActionRow( + title: connectingProvider == BusyProvider.appleICloud + ? l10n.waitingForAppleICloud + : l10n.addAppleICloudAccount, + leading: const Icon(YaruIcons.plus), + onTap: connecting ? null : onAddApple, + ), + BusyMaxActionRow( + title: connectingProvider == BusyProvider.nextcloud + ? l10n.waitingForNextcloud + : l10n.addNextcloudAccount, + leading: const Icon(YaruIcons.plus), + onTap: connecting ? null : onAddNextcloud, + ), + if (connecting) BusyMaxActionRow( - title: connectingProvider == TaskProvider.microsoft - ? l10n.waitingForMicrosoftSignIn - : l10n.addMicrosoftAccount, - leading: const Icon(YaruIcons.plus), - onTap: connecting ? null : onAddMicrosoft, + title: l10n.cancelAccountConnection, + leading: const Icon(YaruIcons.window_close), + onTap: onCancelConnection, ), if (accounts.isEmpty) BusyMaxActionRow( @@ -878,16 +1074,46 @@ class _AccountManagementSection extends StatelessWidget { ), ], ), - for (final account in accounts) + for (final account in accounts) ...[ _AccountManagementCard( account: account, removing: removingAccountIds.contains(account.id), onReconnect: connecting || removingAccountIds.contains(account.id) ? null : () => onReconnect(account), - onCreateTaskList: () => onCreateTaskList(account.id), + onCreateTaskList: + account.provider == BusyProvider.google || + account.provider == BusyProvider.microsoft || + account.provider == BusyProvider.nextcloud + ? () => onCreateTaskList(account.id) + : null, + onRefreshCollections: + account.provider == BusyProvider.appleICloud || + account.provider == BusyProvider.nextcloud + ? () => onRefreshCollections(account) + : null, onRemoveAccount: () => onRemoveAccount(account), ), + if (account.provider == BusyProvider.appleICloud || + account.provider == BusyProvider.nextcloud) + _DavCollectionsCard( + account: account, + collections: [ + for (final collection in davCollections) + if (collection.accountId == account.id) collection, + ], + onEventsSelected: onEventsSelected, + onTasksSelected: onTasksSelected, + ), + if (davConflicts.any((conflict) => conflict.accountId == account.id)) + _DavConflictsCard( + conflicts: [ + for (final conflict in davConflicts) + if (conflict.accountId == account.id) conflict, + ], + onResolve: onResolveConflict, + ), + ], ], ); } @@ -899,13 +1125,15 @@ class _AccountManagementCard extends StatelessWidget { required this.removing, required this.onReconnect, required this.onCreateTaskList, + required this.onRefreshCollections, required this.onRemoveAccount, }); final AccountEntity account; final bool removing; final VoidCallback? onReconnect; - final VoidCallback onCreateTaskList; + final VoidCallback? onCreateTaskList; + final VoidCallback? onRefreshCollections; final VoidCallback onRemoveAccount; @override @@ -916,23 +1144,63 @@ class _AccountManagementCard extends StatelessWidget { description: _accountIdentityLabel(context, account), filled: true, children: [ - if (account.needsReconnect) + if (account.provider == BusyProvider.nextcloud) + BusyMaxActionRow( + title: l10n.nextcloudProvider, + subtitle: l10n.nextcloudServerHost( + Uri.tryParse(account.authority)?.host ?? account.authority, + ), + leading: const Icon(Icons.cloud_outlined), + ), + if (account.provider == BusyProvider.appleICloud || + account.provider == BusyProvider.nextcloud) + BusyMaxActionRow( + title: l10n.davConnectionState, + subtitle: _accountConnectionStateLabel(context, account), + leading: Icon( + account.hasConnectionIssue + ? YaruIcons.warning + : YaruIcons.checkmark, + ), + ), + if (account.provider == BusyProvider.appleICloud || + account.provider == BusyProvider.nextcloud) BusyMaxActionRow( - title: accountReconnectRequiredActionLabel, - subtitle: accountReconnectRequiredSyncMessage, + title: account.lastSuccessfulSyncAtUtc == null + ? l10n.davNeverSynced + : l10n.davLastSuccessfulSync( + _formatDavDateTime( + context, + account.lastSuccessfulSyncAtUtc!, + ), + ), + leading: const Icon(YaruIcons.sync), + ), + if (account.hasConnectionIssue) + BusyMaxActionRow( + title: account.needsReconnect + ? accountReconnectRequiredActionLabel + : _accountConnectionIssueMessage(context, account), + subtitle: _accountConnectionIssueMessage(context, account), leading: Icon( YaruIcons.refresh, color: Theme.of(context).colorScheme.error, ), - onTap: onReconnect, + onTap: account.needsReconnect ? onReconnect : null, ) - else ...[ + else if (onCreateTaskList != null) ...[ BusyMaxActionRow( title: l10n.newTaskList, leading: const Icon(YaruIcons.plus), onTap: removing ? null : onCreateTaskList, ), ], + if (onRefreshCollections != null) + BusyMaxActionRow( + title: l10n.refreshCollections, + leading: const Icon(YaruIcons.sync), + onTap: removing ? null : onRefreshCollections, + ), BusyMaxActionRow( title: removing ? l10n.removingAccount : l10n.removeAccount, subtitle: l10n.removeAccountDescription, @@ -948,6 +1216,153 @@ class _AccountManagementCard extends StatelessWidget { } } +class _DavCollectionsCard extends StatelessWidget { + const _DavCollectionsCard({ + required this.account, + required this.collections, + required this.onEventsSelected, + required this.onTasksSelected, + }); + + final AccountEntity account; + final List collections; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onEventsSelected; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onTasksSelected; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BusyMaxGroupedList( + title: l10n.collectionSettings, + description: account.displayLabel, + filled: true, + children: [ + for (final collection in collections) ...[ + BusyMaxActionRow( + key: ValueKey('dav-collection-${collection.id}'), + title: collection.name, + subtitle: _davCollectionSummary(context, collection), + leading: _DavCollectionColor(color: collection.color), + ), + if (collection.supportsEvents) + BusyMaxSwitchRow( + key: ValueKey('dav-events-toggle-${collection.id}'), + title: l10n.calendarContent, + subtitle: collection.readOnly ? l10n.readOnlyCalendar : null, + value: collection.eventsSelected, + onChanged: (selected) => onEventsSelected(collection, selected), + leading: const Icon(YaruIcons.calendar), + ), + if (collection.supportsTasks) + BusyMaxSwitchRow( + key: ValueKey('dav-tasks-toggle-${collection.id}'), + title: l10n.taskContent, + subtitle: collection.readOnly + ? l10n.readOnlySharedCollection + : null, + value: collection.tasksSelected, + onChanged: (selected) => onTasksSelected(collection, selected), + leading: const Icon(YaruIcons.checkmark), + ), + ], + if (collections.isEmpty) + BusyMaxActionRow( + title: l10n.collectionSettings, + subtitle: l10n.davNeverSynced, + leading: const Icon(YaruIcons.calendar), + ), + ], + ); + } +} + +class _DavCollectionColor extends StatelessWidget { + const _DavCollectionColor({required this.color}); + + final String? color; + + @override + Widget build(BuildContext context) { + return Semantics( + label: context.l10n.calendar, + child: Container( + width: BusyMaxSizes.iconSm, + height: BusyMaxSizes.iconSm, + decoration: BoxDecoration( + color: _parseDavColor(color) ?? Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + ), + ); + } +} + +class _DavConflictsCard extends StatelessWidget { + const _DavConflictsCard({required this.conflicts, required this.onResolve}); + + final List conflicts; + final void Function( + DavConflictEntity conflict, + DavConflictResolution resolution, + ) + onResolve; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return BusyMaxGroupedList( + title: l10n.syncConflicts, + filled: true, + children: [ + for (final conflict in conflicts) ...[ + BusyMaxActionRow( + key: ValueKey('dav-conflict-${conflict.id}'), + title: conflict.itemTitle, + subtitle: [ + '${conflict.collectionName} · ${conflict.accountLabel}', + if (conflict.remoteChangedAtUtc != null) + l10n.remoteChangedAt( + _formatDavDateTime(context, conflict.remoteChangedAtUtc!), + ), + l10n.localPendingEdit(conflict.localEditSummary), + ].join('\n'), + leading: Icon( + YaruIcons.warning, + color: Theme.of(context).colorScheme.error, + ), + ), + if (conflict.canKeepServer) + BusyMaxActionRow( + title: l10n.keepServerVersion, + leading: const Icon(Icons.cloud_download_outlined), + onTap: () => + onResolve(conflict, DavConflictResolution.keepServer), + ), + if (conflict.canReapplyLocal) + BusyMaxActionRow( + title: l10n.reapplyLocalChange, + leading: const Icon(Icons.cloud_upload_outlined), + onTap: () => + onResolve(conflict, DavConflictResolution.reapplyLocal), + ), + if (conflict.canDuplicate) + BusyMaxActionRow( + title: l10n.duplicateLocalItem, + leading: const Icon(Icons.copy_outlined), + onTap: () => + onResolve(conflict, DavConflictResolution.duplicateLocal), + ), + ], + ], + ); + } +} + Future _afterAccountRemoved( BuildContext context, WidgetRef ref, @@ -955,7 +1370,7 @@ Future _afterAccountRemoved( ) async { final accounts = await ref .read(accountsRepositoryProvider) - .listSignedInAccounts(); + .listVisibleAccounts(); final remaining = accounts .where((account) => account.id != removedAccountId) .toList(); @@ -1028,9 +1443,93 @@ String _accountIdentityLabel(BuildContext context, AccountEntity account) { return context.l10n.signedInAccount; } -String _accountProviderLabel(BuildContext context, TaskProvider provider) { +String _accountProviderLabel(BuildContext context, BusyProvider provider) { return switch (provider) { - TaskProvider.google => context.l10n.googleProvider, - TaskProvider.microsoft => context.l10n.microsoftProvider, + BusyProvider.google => context.l10n.googleProvider, + BusyProvider.microsoft => context.l10n.microsoftProvider, + BusyProvider.appleICloud => context.l10n.appleICloudProvider, + BusyProvider.nextcloud => context.l10n.nextcloudProvider, + }; +} + +String _accountConnectionIssueMessage( + BuildContext context, + AccountEntity account, +) => switch (account.connectionState) { + AccountConnectionState.reauthenticationRequired => + context.l10n.davReauthenticationRequired, + AccountConnectionState.temporarilyUnavailable => + context.l10n.davTemporarilyUnavailable, + AccountConnectionState.permissionChanged => context.l10n.davPermissionChanged, + AccountConnectionState.unsupportedServerProfile => + context.l10n.davUnsupportedServer, + AccountConnectionState.connected || + AccountConnectionState.connecting || + AccountConnectionState.signedOut => '', +}; + +String _accountConnectionStateLabel( + BuildContext context, + AccountEntity account, +) => switch (account.connectionState) { + AccountConnectionState.connected => context.l10n.davConnected, + AccountConnectionState.connecting => context.l10n.davConnecting, + AccountConnectionState.reauthenticationRequired => + context.l10n.davReauthenticationRequired, + AccountConnectionState.temporarilyUnavailable => + context.l10n.davTemporarilyUnavailable, + AccountConnectionState.permissionChanged => context.l10n.davPermissionChanged, + AccountConnectionState.unsupportedServerProfile => + context.l10n.davUnsupportedServer, + AccountConnectionState.signedOut => context.l10n.davSignedOut, +}; + +String _davCollectionSummary( + BuildContext context, + DavCollectionSettingsEntity collection, +) { + final l10n = context.l10n; + final support = switch (( + collection.supportsEvents, + collection.supportsTasks, + )) { + (true, true) => l10n.collectionSupportsEventsAndTasks, + (true, false) => l10n.collectionSupportsEvents, + (false, true) => l10n.collectionSupportsTasks, + _ => l10n.collectionSettings, + }; + final access = collection.readOnly + ? l10n.readOnlyCalendar + : collection.shared + ? l10n.sharedCollection + : l10n.writableCollection; + final sync = collection.syncErrorCode != null + ? l10n.collectionSyncError(collection.syncErrorCode!) + : collection.lastSyncAtUtc == null + ? l10n.davNeverSynced + : l10n.collectionLastSynced( + _formatDavDateTime(context, collection.lastSyncAtUtc!), + ); + return '$support · $access\n$sync'; +} + +String _formatDavDateTime(BuildContext context, DateTime value) { + final local = value.toLocal(); + final material = MaterialLocalizations.of(context); + return '${material.formatMediumDate(local)} ' + '${material.formatTimeOfDay(TimeOfDay.fromDateTime(local))}'; +} + +Color? _parseDavColor(String? source) { + final value = source?.trim().replaceFirst('#', ''); + if (value == null) return null; + final normalized = switch (value.length) { + 6 => 'FF$value', + // CalDAV calendar-color uses RRGGBBAA, while Flutter expects AARRGGBB. + 8 => '${value.substring(6, 8)}${value.substring(0, 6)}', + _ => null, }; + if (normalized == null) return null; + final parsed = int.tryParse(normalized, radix: 16); + return parsed == null ? null : Color(parsed); } diff --git a/lib/src/features/sync/account_sync_operations.dart b/lib/src/features/sync/account_sync_operations.dart index 1b72a10..5774d08 100644 --- a/lib/src/features/sync/account_sync_operations.dart +++ b/lib/src/features/sync/account_sync_operations.dart @@ -1,5 +1,6 @@ typedef AccountSyncAction = Future Function(String accountId, {required bool full}); +typedef AccountUsesDav = Future Function(String accountId); abstract interface class AccountSyncOperations { Future syncAccount(String accountId, {required bool full}); @@ -48,3 +49,48 @@ final class DisabledAccountSyncOperations implements AccountSyncOperations { @override Future syncCalendar(String accountId, {required bool full}) async {} } + +final class RoutingAccountSyncOperations implements AccountSyncOperations { + const RoutingAccountSyncOperations({ + required AccountUsesDav usesDav, + required AccountSyncAction syncDav, + required AccountSyncAction syncTasksRest, + required AccountSyncAction syncCalendarRest, + }) : _usesDav = usesDav, + _syncDav = syncDav, + _syncTasksRest = syncTasksRest, + _syncCalendarRest = syncCalendarRest; + + final AccountUsesDav _usesDav; + final AccountSyncAction _syncDav; + final AccountSyncAction _syncTasksRest; + final AccountSyncAction _syncCalendarRest; + + @override + Future syncAccount(String accountId, {required bool full}) async { + if (await _usesDav(accountId)) { + await _syncDav(accountId, full: full); + return; + } + await _syncTasksRest(accountId, full: full); + await _syncCalendarRest(accountId, full: full); + } + + @override + Future syncCalendar(String accountId, {required bool full}) async { + if (await _usesDav(accountId)) { + await _syncDav(accountId, full: full); + return; + } + await _syncCalendarRest(accountId, full: full); + } + + @override + Future syncTasks(String accountId, {required bool full}) async { + if (await _usesDav(accountId)) { + await _syncDav(accountId, full: full); + return; + } + await _syncTasksRest(accountId, full: full); + } +} diff --git a/lib/src/features/sync/calendar_pending_ops_replayer.dart b/lib/src/features/sync/calendar_pending_ops_replayer.dart index 3e0cac5..96dca6a 100644 --- a/lib/src/features/sync/calendar_pending_ops_replayer.dart +++ b/lib/src/features/sync/calendar_pending_ops_replayer.dart @@ -10,7 +10,7 @@ import '../../calendar_providers/cloud_calendar_client.dart'; import '../../db/app_database.dart'; import '../../google_calendar/google_calendar_errors.dart'; import '../../microsoft_calendar/microsoft_calendar_errors.dart'; -import '../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../calendar/data/calendar_repository.dart'; class CalendarPendingOpsReplayer { @@ -145,7 +145,7 @@ class CalendarPendingOpsReplayer { } bool _isMissingTimeZoneCreateFailure(PendingOp op) { - return op.provider == TaskProvider.google.storageValue && + return op.provider == BusyProvider.google.storageValue && op.entityType == 'event' && op.operationType == 'event.create' && op.lastErrorCode == 'GoogleCalendarApiError' && @@ -421,7 +421,7 @@ class CalendarPendingOpsReplayer { BusyProvider provider, Map raw, ) { - if (provider == TaskProvider.google) { + if (provider == BusyProvider.google) { final start = _mapValue(raw['start']); final end = _mapValue(raw['end']); return { diff --git a/lib/src/features/sync/calendar_sync_engine.dart b/lib/src/features/sync/calendar_sync_engine.dart index 5421f26..9cf3f3f 100644 --- a/lib/src/features/sync/calendar_sync_engine.dart +++ b/lib/src/features/sync/calendar_sync_engine.dart @@ -1,6 +1,6 @@ import '../../calendar_providers/cloud_calendar_client.dart'; import '../../db/app_database.dart'; -import '../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../calendar/data/calendar_repository.dart'; import '../notifications/notification_schedule_service.dart'; import 'calendar_pending_ops_replayer.dart'; @@ -84,14 +84,17 @@ class CalendarSyncEngine { final rangeMatches = state?.rangeStart == rangeStartValue && state?.rangeEnd == rangeEndValue; - final savedCursor = provider == TaskProvider.google - ? state?.googleSyncToken - : state?.microsoftDeltaLink; + final expectedCursorKind = provider == BusyProvider.google + ? 'google_sync_token' + : 'microsoft_delta_link'; + final savedCursor = state?.cursorKind == expectedCursorKind + ? state?.cursorValue + : null; final supportsIncrementalCursor = - provider == TaskProvider.google || calendar.primaryCalendar; + provider == BusyProvider.google || calendar.primaryCalendar; final syncOptionsMatch = - provider != TaskProvider.google || - state?.rawStateJson == _googleExpandedEventsSyncState; + provider != BusyProvider.google || + state?.stateJson == _googleExpandedEventsSyncState; final tokenOrLink = supportsIncrementalCursor && rangeMatches && @@ -101,7 +104,7 @@ class CalendarSyncEngine { : null; final requiresSnapshot = tokenOrLink == null || - (provider == TaskProvider.microsoft && !calendar.primaryCalendar); + (provider == BusyProvider.microsoft && !calendar.primaryCalendar); await _syncCalendarRange( providerCalendarId: calendar.providerCalendarId, primaryCalendar: calendar.primaryCalendar, @@ -154,7 +157,7 @@ class CalendarSyncEngine { preservePendingLocalChanges: true, ); final recurringMasterId = event.providerRecurringEventId; - if (provider == TaskProvider.google && + if (provider == BusyProvider.google && recurringMasterId != null && recurringMasterId.isNotEmpty) { expandedRecurringMasterIds.add(recurringMasterId); @@ -177,7 +180,7 @@ class CalendarSyncEngine { calendarId: providerCalendarId, rangeStart: rangeStart, rangeEnd: rangeEnd, - syncTokenOrDeltaLink: provider == TaskProvider.google + syncTokenOrDeltaLink: provider == BusyProvider.google ? tokenOrLink : next, primaryCalendar: primaryCalendar, @@ -199,12 +202,12 @@ class CalendarSyncEngine { continue; } page = nextPage; - if (provider == TaskProvider.google && page.nextPageTokenOrUrl == next) { + if (provider == BusyProvider.google && page.nextPageTokenOrUrl == next) { break; } } - if (provider == TaskProvider.google) { + if (provider == BusyProvider.google) { await _repository.markGoogleRecurringMastersDeleted( accountId: _accountId, providerCalendarId: providerCalendarId, @@ -217,6 +220,7 @@ class CalendarSyncEngine { provider: provider, providerCalendarId: providerCalendarId, ); + final completedCursor = page.nextSyncTokenOrDeltaLink; await _repository.saveSyncState( accountId: _accountId, provider: provider, @@ -224,14 +228,14 @@ class CalendarSyncEngine { calendarSourceId: sourceId, rangeStart: rangeStart.toIso8601String(), rangeEnd: rangeEnd.toIso8601String(), - googleSyncToken: provider == TaskProvider.google - ? page.nextSyncTokenOrDeltaLink - : null, - microsoftDeltaLink: provider == TaskProvider.microsoft - ? page.nextSyncTokenOrDeltaLink - : null, + cursorKind: completedCursor == null + ? 'snapshot_generation' + : provider == BusyProvider.google + ? 'google_sync_token' + : 'microsoft_delta_link', + cursorValue: completedCursor ?? '0', full: full, - rawStateJson: provider == TaskProvider.google + stateJson: provider == BusyProvider.google ? _googleExpandedEventsSyncState : null, ); diff --git a/lib/src/features/sync/pending_op_resolution_service.dart b/lib/src/features/sync/pending_op_resolution_service.dart index 04016e0..8eaad26 100644 --- a/lib/src/features/sync/pending_op_resolution_service.dart +++ b/lib/src/features/sync/pending_op_resolution_service.dart @@ -1,16 +1,16 @@ import 'dart:convert'; import '../../db/app_database.dart'; -import '../../google_tasks/api/google_tasks_api_client.dart'; -import '../../google_tasks/api/google_tasks_api_error.dart'; import '../task_lists/data/task_lists_repository.dart'; import '../tasks/data/tasks_repository.dart'; +import '../tasks/domain/task_remote_client.dart'; +import '../tasks/domain/task_remote_error.dart'; import 'sync_engine.dart'; class PendingOpResolutionService { PendingOpResolutionService({ required AppDatabase database, - required GoogleTasksApiClient apiClient, + required TaskRemoteClient apiClient, required String accountId, required SyncEngine syncEngine, DateTime Function()? nowUtc, @@ -21,7 +21,7 @@ class PendingOpResolutionService { _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); final AppDatabase _database; - final GoogleTasksApiClient _apiClient; + final TaskRemoteClient _apiClient; final String _accountId; final SyncEngine _syncEngine; final DateTime Function() _nowUtc; @@ -78,7 +78,7 @@ class PendingOpResolutionService { taskId, ); } - } on GoogleTasksApiError catch (error) { + } on TaskRemoteError catch (error) { if (error.statusCode != 404) { rethrow; } @@ -103,7 +103,7 @@ class PendingOpResolutionService { await _database.tasksDao.upsertTask( taskFromDto(_accountId, taskListId, dto, _now()), ); - } on GoogleTasksApiError catch (error) { + } on TaskRemoteError catch (error) { if (error.statusCode != 404) { rethrow; } @@ -117,7 +117,7 @@ class PendingOpResolutionService { await _database.taskListsDao.upsertTaskList( taskListFromDto(_accountId, dto, _now()), ); - } on GoogleTasksApiError catch (error) { + } on TaskRemoteError catch (error) { if (error.statusCode != 404) { rethrow; } diff --git a/lib/src/features/sync/pending_ops_replayer.dart b/lib/src/features/sync/pending_ops_replayer.dart index 5cbc9d2..2887c54 100644 --- a/lib/src/features/sync/pending_ops_replayer.dart +++ b/lib/src/features/sync/pending_ops_replayer.dart @@ -4,17 +4,18 @@ import 'dart:math'; import 'package:drift/drift.dart'; import '../../db/app_database.dart'; -import '../../google_tasks/api/google_tasks_api_client.dart'; -import '../../google_tasks/api/google_tasks_api_error.dart'; -import '../../google_tasks/api/google_tasks_api_models.dart'; +import '../tasks/domain/task_remote_client.dart'; +import '../tasks/domain/task_remote_error.dart'; +import '../tasks/domain/task_remote_models.dart'; import 'conflict_detector.dart'; import '../task_lists/data/task_lists_repository.dart'; import '../tasks/data/tasks_repository.dart'; +import '../tasks/domain/task_checklist_item.dart'; class PendingOpsReplayer { PendingOpsReplayer({ required AppDatabase database, - required GoogleTasksApiClient apiClient, + required TaskRemoteClient apiClient, required String accountId, Future Function(String summary)? onConflictBlocked, Random? random, @@ -27,7 +28,7 @@ class PendingOpsReplayer { _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); final AppDatabase _database; - final GoogleTasksApiClient _apiClient; + final TaskRemoteClient _apiClient; final String _accountId; final Future Function(String summary)? _onConflictBlocked; final Random _random; @@ -53,7 +54,7 @@ class PendingOpsReplayer { await _replay(op); await _database.pendingOpsDao.deleteOp(op.id); applied += 1; - } on GoogleTasksApiError catch (error) { + } on TaskRemoteError catch (error) { if (_isSuccessfulMissingDelete(op, error)) { await _applyDeleteSideEffect(op); await _database.pendingOpsDao.deleteOp(op.id); @@ -78,7 +79,9 @@ class PendingOpsReplayer { } bool _isTaskOp(PendingOp op) { - return op.entityType == 'task' || op.entityType == 'task_list'; + return op.entityType == 'task' || + op.entityType == 'task_list' || + op.entityType == 'task_checklist_item'; } Future _replay(PendingOp op) async { @@ -103,6 +106,12 @@ class PendingOpsReplayer { await _moveTask(op); case 'clear_completed_tasks': await _clearCompleted(op); + case 'create_task_checklist_item': + await _createChecklistItem(op); + case 'patch_task_checklist_item': + await _patchChecklistItem(op); + case 'delete_task_checklist_item': + await _deleteChecklistItem(op); default: await _blockOp(op, 'unknown_operation', op.operation); throw const _PendingOpBlocked(); @@ -293,15 +302,228 @@ class PendingOpsReplayer { await _apiClient.clearCompletedTasks(op.taskListId!); } + Future _createChecklistItem(PendingOp op) async { + final request = _request(op); + final body = _requestBody(request); + final item = await _checklistClient.createChecklistItem( + taskListId: op.taskListId!, + taskId: op.taskId!, + title: body['displayName']?.toString() ?? '', + completed: body['isChecked'] == true, + ); + final localId = request['checklistItemId']?.toString() ?? op.localTempId; + await _database.transaction(() async { + await _replaceChecklistProjectionItem( + taskListId: op.taskListId!, + parentTaskId: op.taskId!, + oldItemId: localId, + item: taskChecklistItemFromDto(item), + ); + if (localId != null) { + await _replaceChecklistPendingReference( + parentTaskId: op.taskId!, + oldValue: localId, + newValue: item.id, + ); + } + }); + } + + Future _patchChecklistItem(PendingOp op) async { + final request = _request(op); + final body = _requestBody(request); + final itemId = request['checklistItemId']?.toString(); + if (itemId == null || itemId.isEmpty) { + throw const TaskRemoteError( + statusCode: 400, + code: 'invalid_checklist_item', + message: 'The checklist item identifier is missing.', + ); + } + final item = await _checklistClient.updateChecklistItem( + taskListId: op.taskListId!, + taskId: op.taskId!, + checklistItemId: itemId, + title: body.containsKey('displayName') + ? body['displayName']?.toString() + : null, + completed: body.containsKey('isChecked') + ? body['isChecked'] == true + : null, + ); + await _replaceChecklistProjectionItem( + taskListId: op.taskListId!, + parentTaskId: op.taskId!, + oldItemId: itemId, + item: taskChecklistItemFromDto(item), + ); + } + + Future _deleteChecklistItem(PendingOp op) async { + final itemId = _request(op)['checklistItemId']?.toString(); + if (itemId == null || itemId.isEmpty) { + throw const TaskRemoteError( + statusCode: 400, + code: 'invalid_checklist_item', + message: 'The checklist item identifier is missing.', + ); + } + await _checklistClient.deleteChecklistItem( + taskListId: op.taskListId!, + taskId: op.taskId!, + checklistItemId: itemId, + ); + await _removeChecklistProjectionItem(op, itemId); + } + + TaskChecklistRemoteClient get _checklistClient { + final client = _apiClient; + if (client is TaskChecklistRemoteClient) { + return client as TaskChecklistRemoteClient; + } + throw const TaskRemoteError( + statusCode: 400, + code: 'unsupported_provider_operation', + message: 'This task provider does not expose checklist subtasks.', + ); + } + + Map _requestBody(Map request) { + final body = request['body']; + if (body is Map) return body.cast(); + return const {}; + } + + Future _replaceChecklistProjectionItem({ + required String taskListId, + required String parentTaskId, + required String? oldItemId, + required TaskChecklistItemEntity item, + }) async { + final task = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(parentTaskId), + )) + .getSingleOrNull(); + if (task == null) return; + final items = decodeTaskChecklistItems(task.microsoftChecklistItemsJson); + final index = oldItemId == null + ? -1 + : items.indexWhere((candidate) => candidate.id == oldItemId); + if (index < 0) { + items.add(item); + } else { + items[index] = item; + } + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(parentTaskId), + )) + .write( + TasksCompanion( + microsoftChecklistItemsJson: Value(encodeTaskChecklistItems(items)), + updatedLocalAtUtc: Value(_now()), + ), + ); + } + + Future _removeChecklistProjectionItem( + PendingOp op, + String itemId, + ) async { + final task = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(op.taskListId!) & + row.id.equals(op.taskId!), + )) + .getSingleOrNull(); + if (task == null) return; + final items = decodeTaskChecklistItems(task.microsoftChecklistItemsJson); + items.removeWhere((item) => item.id == itemId); + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(op.taskListId!) & + row.id.equals(op.taskId!), + )) + .write( + TasksCompanion( + microsoftChecklistItemsJson: Value(encodeTaskChecklistItems(items)), + updatedLocalAtUtc: Value(_now()), + ), + ); + } + + Future _replaceChecklistPendingReference({ + required String parentTaskId, + required String oldValue, + required String newValue, + }) async { + final operations = + await (_database.select(_database.pendingOps)..where( + (row) => + row.accountId.equals(_accountId) & + row.entityType.equals('task_checklist_item') & + row.taskId.equals(parentTaskId), + )) + .get(); + for (final operation in operations) { + final request = _request(operation); + final rewritten = _replaceJsonReference(request, oldValue, newValue); + await (_database.update( + _database.pendingOps, + )..where((row) => row.id.equals(operation.id))).write( + PendingOpsCompanion( + localTempId: operation.localTempId == oldValue + ? Value(newValue) + : const Value.absent(), + requestJson: Value(jsonEncode(rewritten)), + updatedAtUtc: Value(_now()), + ), + ); + } + } + Future _replaceLocalTaskId({ required String taskListId, required String tempTaskId, required TaskDto serverTask, }) async { await _database.transaction(() async { + final localTask = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(tempTaskId), + )) + .getSingleOrNull(); await _database.tasksDao.upsertTask( taskFromDto(_accountId, taskListId, serverTask, _now()), ); + if (localTask?.microsoftChecklistItemsJson != null) { + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(serverTask.id), + )) + .write( + TasksCompanion( + microsoftChecklistItemsJson: Value( + localTask!.microsoftChecklistItemsJson, + ), + updatedLocalAtUtc: Value(_now()), + ), + ); + } await _database.customStatement( 'UPDATE tasks SET parent = ? WHERE account_id = ? AND parent = ?', [serverTask.id, _accountId, tempTaskId], @@ -364,11 +586,21 @@ class PendingOpsReplayer { op.taskId!, ); } + if (op.operation == 'delete_task_checklist_item' && + op.taskListId != null && + op.taskId != null) { + final itemId = _request(op)['checklistItemId']?.toString(); + if (itemId != null) { + await _removeChecklistProjectionItem(op, itemId); + } + } } - bool _isSuccessfulMissingDelete(PendingOp op, GoogleTasksApiError error) { + bool _isSuccessfulMissingDelete(PendingOp op, TaskRemoteError error) { return error.statusCode == 404 && - (op.operation == 'delete_task_list' || op.operation == 'delete_task'); + (op.operation == 'delete_task_list' || + op.operation == 'delete_task' || + op.operation == 'delete_task_checklist_item'); } bool _isRetryableStatus(int statusCode) { @@ -645,6 +877,7 @@ const _pendingOpReferenceKeys = { 'taskListId', 'tasklist', 'destinationTasklist', + 'checklistItemId', }; Object? _replaceJsonReference( diff --git a/lib/src/features/sync/sync_auth_error.dart b/lib/src/features/sync/sync_auth_error.dart index 1298c63..3fd1eb3 100644 --- a/lib/src/features/sync/sync_auth_error.dart +++ b/lib/src/features/sync/sync_auth_error.dart @@ -1,5 +1,5 @@ import '../../core/logging/redacting_logger.dart'; -import '../../google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; const accountReconnectRequiredSyncMessage = 'This account needs to be reconnected.'; diff --git a/lib/src/features/sync/sync_engine.dart b/lib/src/features/sync/sync_engine.dart index cd3877b..386c340 100644 --- a/lib/src/features/sync/sync_engine.dart +++ b/lib/src/features/sync/sync_engine.dart @@ -2,16 +2,18 @@ import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; import '../../db/app_database.dart'; -import '../../google_tasks/api/google_tasks_api_client.dart'; import '../notifications/notification_schedule_service.dart'; import '../task_lists/data/task_lists_repository.dart'; import '../tasks/data/tasks_repository.dart'; +import '../tasks/domain/task_checklist_item.dart'; +import '../tasks/domain/task_remote_client.dart'; +import '../tasks/domain/task_remote_models.dart'; import 'pending_ops_replayer.dart'; class SyncEngine { SyncEngine({ required AppDatabase database, - required GoogleTasksApiClient apiClient, + required TaskRemoteClient apiClient, required String accountId, bool fullRefreshOnly = false, Future Function(String summary)? onConflictBlocked, @@ -26,7 +28,7 @@ class SyncEngine { _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); final AppDatabase _database; - final GoogleTasksApiClient _apiClient; + final TaskRemoteClient _apiClient; final String _accountId; final bool _fullRefreshOnly; final Future Function(String summary)? _onConflictBlocked; @@ -153,6 +155,64 @@ class SyncEngine { return seen; } + Future _pullChecklistItems(String taskListId, String taskId) async { + final client = _apiClient as TaskChecklistRemoteClient; + final serverItems = []; + String? pageToken; + do { + final page = await client.listChecklistItemsPage( + taskListId: taskListId, + taskId: taskId, + pageToken: pageToken, + ); + serverItems.addAll(page.items); + pageToken = page.nextPageToken; + } while (pageToken != null && pageToken.isNotEmpty); + + final task = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(taskId), + )) + .getSingleOrNull(); + if (task == null) return; + final pending = + await (_database.select(_database.pendingOps) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.entityType.equals('task_checklist_item') & + row.taskListId.equals(taskListId) & + row.taskId.equals(taskId), + ) + ..orderBy([ + (row) => OrderingTerm.asc(row.createdAtUtc), + (row) => OrderingTerm.asc(row.updatedAtUtc), + ])) + .get(); + final merged = mergeTaskChecklistProjection( + serverItems: serverItems, + localItems: decodeTaskChecklistItems(task.microsoftChecklistItemsJson), + pendingOperations: pending, + ); + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(taskId), + )) + .write( + TasksCompanion( + microsoftChecklistItemsJson: Value( + encodeTaskChecklistItems(merged), + ), + updatedLocalAtUtc: Value(_now()), + ), + ); + } + Future> _pullTasks( String taskListId, { required DateTime? updatedMin, @@ -183,6 +243,12 @@ class SyncEngine { pageToken = page.nextPageToken; } while (pageToken != null && pageToken.isNotEmpty); + if (_apiClient is TaskChecklistRemoteClient) { + for (final taskId in seen) { + await _pullChecklistItems(taskListId, taskId); + } + } + return seen; } diff --git a/lib/src/features/task_lists/data/task_lists_repository.dart b/lib/src/features/task_lists/data/task_lists_repository.dart index 9cfc085..5de218e 100644 --- a/lib/src/features/task_lists/data/task_lists_repository.dart +++ b/lib/src/features/task_lists/data/task_lists_repository.dart @@ -4,8 +4,10 @@ import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; import '../../../db/app_database.dart'; -import '../../../google_tasks/api/google_tasks_api_client.dart'; -import '../../../google_tasks/api/google_tasks_api_models.dart'; +import '../../../dav/mutation/dav_task_list_mutation_service.dart'; +import '../../../providers/busy_provider.dart'; +import '../../tasks/domain/task_remote_client.dart'; +import '../../tasks/domain/task_remote_models.dart'; class TaskListEntity { const TaskListEntity({ @@ -20,6 +22,7 @@ class TaskListEntity { this.providerListKind, this.isOwner, this.isShared, + this.davCollectionId, }); factory TaskListEntity.fromRow(TaskList row) { @@ -35,6 +38,7 @@ class TaskListEntity { providerListKind: row.providerListKind, isOwner: row.isOwner, isShared: row.isShared, + davCollectionId: row.davCollectionId, ); } @@ -49,6 +53,7 @@ class TaskListEntity { final String? providerListKind; final bool? isOwner; final bool? isShared; + final String? davCollectionId; bool get isMicrosoftBuiltIn => providerListKind == 'defaultList' || providerListKind == 'flaggedEmails'; @@ -61,20 +66,23 @@ class TaskListsRepository { TaskListsRepository({ required AppDatabase database, required String accountId, - GoogleTasksApiClient? apiClient, + TaskRemoteClient? apiClient, + DavTaskListMutationClient? davMutationClient, void Function()? onMutationQueued, Uuid uuid = const Uuid(), DateTime Function()? nowUtc, }) : _database = database, _accountId = accountId, _apiClient = apiClient, + _davMutationClient = davMutationClient, _onMutationQueued = onMutationQueued, _uuid = uuid, _nowUtc = nowUtc ?? (() => DateTime.now().toUtc()); final AppDatabase _database; final String _accountId; - final GoogleTasksApiClient? _apiClient; + final TaskRemoteClient? _apiClient; + final DavTaskListMutationClient? _davMutationClient; final void Function()? _onMutationQueued; final Uuid _uuid; final DateTime Function() _nowUtc; @@ -108,6 +116,10 @@ class TaskListsRepository { } Future createTaskList(String title) async { + if (await _usesNextcloudDav()) { + await _requiredDavMutationClient().createTaskList(title); + return; + } final now = _now(); final localId = 'local-tasklist-${_uuid.v4()}'; await _database.transaction(() async { @@ -136,6 +148,13 @@ class TaskListsRepository { Future renameTaskList(String id, String title) async { final now = _now(); final baseline = await _baselineRow(id); + if (baseline?.davCollectionId != null) { + await _requiredDavMutationClient().renameTaskList( + baseline!.davCollectionId!, + title, + ); + return; + } await _updateLocalList( id, TaskListsCompanion( @@ -159,6 +178,18 @@ class TaskListsRepository { final now = _now(); final baseline = await _baselineRow(id); final title = replacement.fields['title']?.toString(); + if (baseline?.davCollectionId != null) { + if (title == null) { + throw ArgumentError( + 'A complete DAV task-list update must include its title.', + ); + } + await _requiredDavMutationClient().renameTaskList( + baseline!.davCollectionId!, + title, + ); + return; + } if (title != null) { await _updateLocalList( id, @@ -183,6 +214,12 @@ class TaskListsRepository { Future deleteTaskList(String id) async { final now = _now(); final baseline = await _baselineRow(id); + if (baseline?.davCollectionId != null) { + await _requiredDavMutationClient().deleteTaskList( + baseline!.davCollectionId!, + ); + return; + } await _updateLocalList( id, TaskListsCompanion( @@ -253,6 +290,23 @@ class TaskListsRepository { .getSingleOrNull(); } + Future _usesNextcloudDav() async { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingleOrNull(); + return account != null && + BusyProviderCodec.requireStorageValue(account.provider) == + BusyProvider.nextcloud; + } + + DavTaskListMutationClient _requiredDavMutationClient() { + final client = _davMutationClient; + if (client == null) { + throw StateError('The Nextcloud task-list mutation client is missing.'); + } + return client; + } + String _now() => _nowUtc().toIso8601String(); } diff --git a/lib/src/features/tasks/data/tasks_repository.dart b/lib/src/features/tasks/data/tasks_repository.dart index 673a8b7..cd762bb 100644 --- a/lib/src/features/tasks/data/tasks_repository.dart +++ b/lib/src/features/tasks/data/tasks_repository.dart @@ -4,11 +4,19 @@ import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; +import '../../../dav/ical/ical_document.dart'; +import '../../../dav/ical/ical_semantics.dart'; +import '../../../dav/mutation/dav_conditional_mutation_service.dart'; +import '../../../dav/mutation/dav_mutation_patch.dart'; +import '../../../dav/mutation/dav_pending_operations.dart'; +import '../../../dav/mutation/dav_projection_mutations.dart'; +import '../../../dav/storage/dav_object_repository.dart'; import '../../../db/app_database.dart'; -import '../../../google_tasks/api/google_tasks_api_client.dart'; -import '../../../google_tasks/api/google_tasks_api_models.dart'; import '../../../google_tasks/api/google_tasks_json.dart'; -import '../../../task_providers/task_provider.dart'; +import '../domain/task_checklist_item.dart'; +import '../domain/task_remote_client.dart'; +import '../domain/task_remote_models.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import '../../notifications/notification_schedule_service.dart'; class TaskTreeNode { @@ -18,6 +26,57 @@ class TaskTreeNode { final List children; } +enum TaskSubtaskKind { task, checklistItem } + +class TaskSubtaskEntity { + const TaskSubtaskEntity._({ + required this.kind, + required this.id, + required this.title, + required this.completed, + required this.hasChildren, + this.task, + this.checklistItem, + }); + + factory TaskSubtaskEntity.task(TaskEntity task, {required bool hasChildren}) { + return TaskSubtaskEntity._( + kind: TaskSubtaskKind.task, + id: task.id, + title: task.title, + completed: task.status == 'completed', + hasChildren: hasChildren, + task: task, + ); + } + + factory TaskSubtaskEntity.checklistItem(TaskChecklistItemEntity item) { + return TaskSubtaskEntity._( + kind: TaskSubtaskKind.checklistItem, + id: item.id, + title: item.title, + completed: item.completed, + hasChildren: false, + checklistItem: item, + ); + } + + final TaskSubtaskKind kind; + final String id; + final String title; + final bool completed; + final bool hasChildren; + final TaskEntity? task; + final TaskChecklistItemEntity? checklistItem; +} + +class TaskHierarchySnapshot { + const TaskHierarchySnapshot({required this.parent, required this.subtasks}); + + final TaskEntity? parent; + final List subtasks; +} + class TaskTreeGroup { const TaskTreeGroup({ required this.accountId, @@ -30,7 +89,7 @@ class TaskTreeGroup { final String accountId; final String accountLabel; - final TaskProvider provider; + final BusyProvider provider; final String taskListId; final String taskListTitle; final List nodes; @@ -47,6 +106,21 @@ class TaskEntity { required this.pendingMove, required this.rawJson, required this.updatedLocalAtUtc, + this.davCollectionId, + this.davObjectId, + this.icalUid, + this.recurrenceIdKey, + this.icalPriority, + this.percentComplete, + this.taskLocation, + this.taskUrl, + this.taskClassification, + this.taskPinned, + this.taskHideSubtasks, + this.taskHideCompletedSubtasks, + this.taskAlarmsJson, + this.parentUid, + this.sortOrder, this.etag, this.updatedUtc, this.selfLink, @@ -68,6 +142,7 @@ class TaskEntity { this.microsoftIsReminderOn, this.microsoftCompletedDateTime, this.microsoftCompletedTimeZone, + this.microsoftChecklistItems = const [], this.recurrenceJson, this.importance, this.categoriesJson, @@ -81,6 +156,7 @@ class TaskEntity { }); factory TaskEntity.fromRow(Task row) { + final native = _davNativeTaskFields(row.providerMetadataJson); return TaskEntity( accountId: row.accountId, taskListId: row.taskListId, @@ -91,6 +167,21 @@ class TaskEntity { pendingMove: row.pendingMove, rawJson: row.rawJson, updatedLocalAtUtc: row.updatedLocalAtUtc, + davCollectionId: row.davCollectionId, + davObjectId: row.davObjectId, + icalUid: row.icalUid, + recurrenceIdKey: row.recurrenceIdKey, + icalPriority: row.icalPriority, + percentComplete: row.percentComplete, + taskLocation: row.taskLocation, + taskUrl: row.taskUrl, + taskClassification: row.taskClassification, + taskPinned: row.taskPinned, + taskHideSubtasks: row.taskHideSubtasks, + taskHideCompletedSubtasks: row.taskHideCompletedSubtasks, + taskAlarmsJson: row.taskAlarmsJson, + parentUid: row.parentUid, + sortOrder: row.sortOrder, etag: row.etag, updatedUtc: row.updatedUtc, selfLink: row.selfLink, @@ -103,15 +194,20 @@ class TaskEntity { providerStatus: row.providerStatus, bodyContent: row.bodyContent, bodyContentType: row.bodyContentType, - microsoftDueDateTime: row.microsoftDueDateTime, - microsoftDueTimeZone: row.microsoftDueTimeZone, - microsoftStartDateTime: row.microsoftStartDateTime, - microsoftStartTimeZone: row.microsoftStartTimeZone, + microsoftDueDateTime: row.microsoftDueDateTime ?? native.dueDateTime, + microsoftDueTimeZone: row.microsoftDueTimeZone ?? native.dueTimeZone, + microsoftStartDateTime: + row.microsoftStartDateTime ?? native.startDateTime, + microsoftStartTimeZone: + row.microsoftStartTimeZone ?? native.startTimeZone, microsoftReminderDateTime: row.microsoftReminderDateTime, microsoftReminderTimeZone: row.microsoftReminderTimeZone, microsoftIsReminderOn: row.microsoftIsReminderOn, microsoftCompletedDateTime: row.microsoftCompletedDateTime, microsoftCompletedTimeZone: row.microsoftCompletedTimeZone, + microsoftChecklistItems: decodeTaskChecklistItems( + row.microsoftChecklistItemsJson, + ), recurrenceJson: row.recurrenceJson, importance: row.importance, categoriesJson: row.categoriesJson, @@ -134,6 +230,21 @@ class TaskEntity { final bool pendingMove; final String rawJson; final String updatedLocalAtUtc; + final String? davCollectionId; + final String? davObjectId; + final String? icalUid; + final String? recurrenceIdKey; + final int? icalPriority; + final int? percentComplete; + final String? taskLocation; + final String? taskUrl; + final String? taskClassification; + final bool? taskPinned; + final bool? taskHideSubtasks; + final bool? taskHideCompletedSubtasks; + final String? taskAlarmsJson; + final String? parentUid; + final int? sortOrder; final String? etag; final String? updatedUtc; final String? selfLink; @@ -155,6 +266,7 @@ class TaskEntity { final bool? microsoftIsReminderOn; final String? microsoftCompletedDateTime; final String? microsoftCompletedTimeZone; + final List microsoftChecklistItems; final String? recurrenceJson; final String? importance; final String? categoriesJson; @@ -232,6 +344,21 @@ class TaskMoveInput { final String? destinationTaskListId; } +class DavTaskBatchMutationException implements Exception { + const DavTaskBatchMutationException({ + required this.appliedCount, + required this.failedCount, + }); + + final int appliedCount; + final int failedCount; + + @override + String toString() => + 'Some completed tasks could not be queued for deletion ' + '($appliedCount queued, $failedCount failed).'; +} + class TaskViewFilter { const TaskViewFilter({ this.showCompleted = true, @@ -262,7 +389,7 @@ class TasksRepository { TasksRepository({ required AppDatabase database, required String accountId, - GoogleTasksApiClient? apiClient, + TaskRemoteClient? apiClient, void Function()? onMutationQueued, Future Function()? onNotificationScheduleChanged, Uuid uuid = const Uuid(), @@ -277,7 +404,7 @@ class TasksRepository { final AppDatabase _database; final String _accountId; - final GoogleTasksApiClient? _apiClient; + final TaskRemoteClient? _apiClient; final void Function()? _onMutationQueued; final Future Function()? _onNotificationScheduleChanged; final Uuid _uuid; @@ -293,6 +420,59 @@ class TasksRepository { }); } + Stream watchTaskHierarchy( + String taskListId, + String taskId, + ) { + return _database.tasksDao.watchTaskTree(_accountId, taskListId).map((rows) { + final entities = rows + .where((row) => row.deleted != true && row.hidden != true) + .map(TaskEntity.fromRow) + .toList(); + final current = entities.firstWhereOrNull((task) => task.id == taskId); + if (current == null) { + return const TaskHierarchySnapshot(parent: null, subtasks: []); + } + final parent = entities.firstWhereOrNull( + (task) => + task.id == current.parent || + (current.parentUid != null && task.icalUid == current.parentUid), + ); + final taskChildren = + entities + .where( + (task) => + task.parent == current.id || + (current.icalUid != null && + task.parentUid == current.icalUid), + ) + .toList() + ..sort(_compareTaskOrder); + final taskSubtasks = [ + for (final child in taskChildren) + TaskSubtaskEntity.task( + child, + hasChildren: + entities.any( + (candidate) => + candidate.parent == child.id || + (child.icalUid != null && + candidate.parentUid == child.icalUid), + ) || + child.microsoftChecklistItems.isNotEmpty, + ), + ]; + final checklistSubtasks = [ + for (final item in current.microsoftChecklistItems) + TaskSubtaskEntity.checklistItem(item), + ]; + return TaskHierarchySnapshot( + parent: parent, + subtasks: [...taskSubtasks, ...checklistSubtasks], + ); + }); + } + Stream> watchAllTaskTreeGroups( List accountIds, TaskViewFilter filter, @@ -316,7 +496,7 @@ class TasksRepository { TaskTreeGroup( accountId: rows.first.account.id, accountLabel: _accountLabel(rows.first.account), - provider: TaskProviderParsing.fromStorageValue( + provider: BusyProviderCodec.requireStorageValue( rows.first.account.provider, ), taskListId: rows.first.taskList.id, @@ -364,6 +544,10 @@ class TasksRepository { } Future createTask(String taskListId, TaskCreateInput input) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId != null) { + return _createDavTask(taskList, input); + } final now = _now(); final localId = 'local-task-${_uuid.v4()}'; final fields = input.toFields(); @@ -385,7 +569,7 @@ class TasksRepository { ), ); await _patchLocalTask(taskListId, localId, fields, now); - await _enqueue( + final createOperationId = await _enqueue( operation: 'create_task', taskListId: taskListId, taskId: localId, @@ -398,16 +582,194 @@ class TasksRepository { }, createdAtUtc: now, ); + if (input.parentTaskId != null) { + final moveCreatedAt = DateTime.parse( + now, + ).add(const Duration(milliseconds: 1)).toIso8601String(); + await _enqueue( + operation: 'move_task', + taskListId: taskListId, + taskId: localId, + request: { + 'parent': input.parentTaskId, + if (input.previousSiblingTaskId != null) + 'previous': input.previousSiblingTaskId, + }, + createdAtUtc: moveCreatedAt, + dependsOnOpId: createOperationId, + ); + } }); await _rebuildTaskNotifications(); _onMutationQueued?.call(); } + Future createSubtask({ + required String taskListId, + required String parentTaskId, + required String title, + }) async { + final normalizedTitle = title.trim(); + if (normalizedTitle.isEmpty) { + throw ArgumentError.value(title, 'title'); + } + if (_apiClient is TaskChecklistRemoteClient) { + await _createChecklistSubtask( + taskListId: taskListId, + parentTaskId: parentTaskId, + title: normalizedTitle, + ); + return; + } + await createTask( + taskListId, + TaskCreateInput(title: normalizedTitle, parentTaskId: parentTaskId), + ); + } + + Future patchChecklistSubtask({ + required String taskListId, + required String parentTaskId, + required String checklistItemId, + String? title, + bool? completed, + }) async { + if (_apiClient is! TaskChecklistRemoteClient) { + throw UnsupportedError('This provider does not use checklist subtasks.'); + } + final normalizedTitle = title?.trim(); + if (normalizedTitle != null && normalizedTitle.isEmpty) { + throw ArgumentError.value(title, 'title'); + } + if (normalizedTitle == null && completed == null) return; + + final task = await _requiredTask(taskListId, parentTaskId); + final items = List.of( + decodeTaskChecklistItems(task.microsoftChecklistItemsJson), + ); + final index = items.indexWhere((item) => item.id == checklistItemId); + if (index < 0) { + throw StateError('The checklist subtask is unavailable.'); + } + final now = _now(); + final raw = items[index].toJson(); + if (normalizedTitle != null) raw['displayName'] = normalizedTitle; + if (completed != null) { + raw['isChecked'] = completed; + if (completed) { + raw['checkedDateTime'] = now; + } else { + raw.remove('checkedDateTime'); + } + } + items[index] = TaskChecklistItemEntity.fromJson(raw); + final body = { + if (normalizedTitle != null) 'displayName': normalizedTitle, + if (completed != null) 'isChecked': completed, + }; + await _database.transaction(() async { + await _writeChecklistProjection(taskListId, parentTaskId, items, now); + await _enqueueChecklistOperation( + operation: 'patch_task_checklist_item', + taskListId: taskListId, + parentTaskId: parentTaskId, + checklistItemId: checklistItemId, + request: {'checklistItemId': checklistItemId, 'body': body}, + createdAtUtc: now, + ); + }); + _onMutationQueued?.call(); + } + + Future deleteChecklistSubtask({ + required String taskListId, + required String parentTaskId, + required String checklistItemId, + }) async { + if (_apiClient is! TaskChecklistRemoteClient) { + throw UnsupportedError('This provider does not use checklist subtasks.'); + } + final task = await _requiredTask(taskListId, parentTaskId); + final items = decodeTaskChecklistItems(task.microsoftChecklistItemsJson); + if (!items.any((item) => item.id == checklistItemId)) { + throw StateError('The checklist subtask is unavailable.'); + } + final now = _now(); + final pendingCreate = await _pendingChecklistCreate( + taskListId, + parentTaskId, + checklistItemId, + ); + await _database.transaction(() async { + await _writeChecklistProjection(taskListId, parentTaskId, [ + for (final item in items) + if (item.id != checklistItemId) item, + ], now); + if (pendingCreate != null) { + await _deleteChecklistOperationChain( + parentTaskId: parentTaskId, + checklistItemId: checklistItemId, + ); + } else { + await _enqueueChecklistOperation( + operation: 'delete_task_checklist_item', + taskListId: taskListId, + parentTaskId: parentTaskId, + checklistItemId: checklistItemId, + request: {'checklistItemId': checklistItemId}, + createdAtUtc: now, + ); + } + }); + _onMutationQueued?.call(); + } + + Future _createChecklistSubtask({ + required String taskListId, + required String parentTaskId, + required String title, + }) async { + final task = await _requiredTask(taskListId, parentTaskId); + final items = List.of( + decodeTaskChecklistItems(task.microsoftChecklistItemsJson), + ); + final now = _now(); + final localId = 'local-checklist-${_uuid.v4()}'; + items.add( + TaskChecklistItemEntity.fromJson({ + 'id': localId, + 'displayName': title, + 'isChecked': false, + 'createdDateTime': now, + }), + ); + await _database.transaction(() async { + await _writeChecklistProjection(taskListId, parentTaskId, items, now); + await _enqueueChecklistOperation( + operation: 'create_task_checklist_item', + taskListId: taskListId, + parentTaskId: parentTaskId, + checklistItemId: localId, + localTempId: localId, + request: { + 'checklistItemId': localId, + 'body': {'displayName': title, 'isChecked': false}, + }, + createdAtUtc: now, + ); + }); + _onMutationQueued?.call(); + } + Future patchTask( String taskListId, String taskId, TaskPatchInput input, ) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId != null) { + return _updateDavTaskWithHierarchy(taskList, taskId, input.fields); + } final now = _now(); await _database.transaction(() async { final baseline = await _baselineRow(taskListId, taskId); @@ -431,6 +793,10 @@ class TasksRepository { String taskId, TaskPutInput input, ) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId != null) { + return _updateDavTaskWithHierarchy(taskList, taskId, input.fields); + } final now = _now(); await _database.transaction(() async { final baseline = await _baselineRow(taskListId, taskId); @@ -450,6 +816,13 @@ class TasksRepository { } Future deleteTask(String taskListId, String taskId) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId != null) { + await _deleteDavTask(taskList, taskId); + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + return; + } final now = _now(); final baseline = await _baselineRow(taskListId, taskId); await _writeLocalTask( @@ -475,6 +848,13 @@ class TasksRepository { } Future moveTask(TaskMoveInput input) async { + final taskList = await _requiredTaskList(input.sourceTaskListId); + if (taskList.davCollectionId != null) { + await _moveDavTask(taskList, input); + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + return; + } final now = _now(); final baseline = await _baselineRow(input.sourceTaskListId, input.taskId); await _writeLocalTask( @@ -509,6 +889,10 @@ class TasksRepository { } Future clearCompleted(String taskListId) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId != null) { + return _clearCompletedDavTasks(taskList); + } final now = _now(); final baselineUpdatedUtc = await _completedTasksBaselineUpdatedUtc( taskListId, @@ -523,158 +907,1471 @@ class TasksRepository { _onMutationQueued?.call(); } - Future refreshTask(String taskListId, String taskId) async { - final apiClient = _apiClient; - if (apiClient == null) { - return; + Future duplicateTask(String taskListId, String taskId) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId == null) { + throw UnsupportedError( + 'Native task duplication is only available for DAV task lists.', + ); } - - final dto = await apiClient.getTask(taskListId: taskListId, taskId: taskId); - await _database.tasksDao.upsertTask( - taskFromDto(_accountId, taskListId, dto, _now()), + final source = await _requiredTask(taskList.id, taskId); + if (source.recurrenceIdKey != null) { + throw UnsupportedError( + 'An individual recurring DAV task occurrence cannot be duplicated.', + ); + } + final existingParent = await _davParentTask(taskList.id, source); + final result = await _duplicateDavTaskNode( + taskList, + source, + parentId: existingParent?.id, + parentUid: existingParent?.icalUid, + dependsOnOperationId: null, ); + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + return result.localId; } - Future _rebuildTaskNotifications() async { - await NotificationScheduleService( + Future nativeTaskExport(String taskListId, String taskId) async { + final taskList = await _requiredTaskList(taskListId); + if (taskList.davCollectionId == null) return null; + final task = await _requiredTask(taskList.id, taskId); + final objectId = task.davObjectId; + if (objectId == null) { + final create = await _pendingDavCreateForProjection(task.id); + return create == null ? null : _pendingDavCreateRawIcs(create); + } + return DavPendingOperationQueue( database: _database, + idFactory: _uuid.v4, nowUtc: _nowUtc, - ).rebuildUpcomingTaskNotifications(_accountId); - await _onNotificationScheduleChanged?.call(); - } - - Future _patchLocalTask( - String taskListId, - String taskId, - Map fields, - String now, - ) { - return _writeLocalTask( - taskListId, - taskId, - TasksCompanion( - title: fields.containsKey('title') - ? Value(fields['title']?.toString() ?? '') - : const Value.absent(), - notes: fields.containsKey('notes') - ? Value(fields['notes']?.toString()) - : const Value.absent(), - status: fields.containsKey('status') - ? Value(fields['status']?.toString()) - : const Value.absent(), - dueUtc: fields.containsKey('due') - ? Value(normalizeGoogleDueDateValue(fields['due'])) - : const Value.absent(), - completedUtc: fields.containsKey('completed') - ? Value(fields['completed']?.toString()) - : const Value.absent(), - providerStatus: fields.containsKey('providerStatus') - ? Value(fields['providerStatus']?.toString()) - : const Value.absent(), - bodyContent: fields.containsKey('bodyContent') - ? Value(fields['bodyContent']?.toString()) - : const Value.absent(), - bodyContentType: fields.containsKey('bodyContentType') - ? Value(fields['bodyContentType']?.toString()) - : const Value.absent(), - microsoftDueDateTime: fields.containsKey('microsoftDueDateTime') - ? Value(_microsoftDateTimeField(fields['microsoftDueDateTime'])) - : const Value.absent(), - microsoftDueTimeZone: fields.containsKey('microsoftDueTimeZone') - ? Value(fields['microsoftDueTimeZone']?.toString()) - : const Value.absent(), - microsoftStartDateTime: fields.containsKey('microsoftStartDateTime') - ? Value(_microsoftDateTimeField(fields['microsoftStartDateTime'])) - : const Value.absent(), - microsoftStartTimeZone: fields.containsKey('microsoftStartTimeZone') - ? Value(fields['microsoftStartTimeZone']?.toString()) - : const Value.absent(), - microsoftReminderDateTime: - fields.containsKey('microsoftReminderDateTime') - ? Value( - _microsoftDateTimeField(fields['microsoftReminderDateTime']), - ) - : const Value.absent(), - microsoftReminderTimeZone: - fields.containsKey('microsoftReminderTimeZone') - ? Value(fields['microsoftReminderTimeZone']?.toString()) - : const Value.absent(), - microsoftIsReminderOn: fields.containsKey('microsoftIsReminderOn') - ? Value(fields['microsoftIsReminderOn'] as bool?) - : const Value.absent(), - recurrenceJson: fields.containsKey('recurrence') - ? Value(_jsonOrNull(fields['recurrence'])) - : const Value.absent(), - importance: fields.containsKey('importance') - ? Value(fields['importance']?.toString()) - : const Value.absent(), - categoriesJson: fields.containsKey('categories') - ? Value(_jsonOrNull(fields['categories'])) - : const Value.absent(), - deleted: fields.containsKey('deleted') - ? Value(fields['deleted'] as bool?) - : const Value.absent(), - localDirty: const Value(true), - updatedLocalAtUtc: Value(now), - ), + ).editableRawIcsForObject( + accountId: _accountId, + collectionId: taskList.davCollectionId!, + objectId: objectId, ); } - Future _writeLocalTask( - String taskListId, - String taskId, - TasksCompanion companion, - ) { - final update = _database.update(_database.tasks) - ..where( - (row) => - row.accountId.equals(_accountId) & - row.taskListId.equals(taskListId) & - row.id.equals(taskId), - ); - return update.write(companion); - } - - Future _enqueue({ - required String operation, - required Map request, - required String createdAtUtc, - String? taskListId, - String? taskId, - String? localTempId, - String? baselineUpdatedUtc, - String? baselineRawJson, - }) async { + Future _createDavTask(TaskList taskList, TaskCreateInput input) async { + final collectionId = taskList.davCollectionId!; + final fields = Map.from(input.toFields()); + await _ensureDavCreateAllowed(taskList, fields); + final parent = await _davParent( + taskList.id, + input.parentTaskId, + childTaskId: null, + ); + if (parent != null && + !_requestsDavTaskCompletion(fields) && + _davTaskCompleted(parent)) { + await _updateDavTaskHierarchyNode(taskList, parent.id, const { + 'percentComplete': 0, + }, visited: {}); + } + final object = buildDavTaskObject( + fields, + parentUid: parent?.icalUid, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ); + final sortOrder = nextcloudTaskSortOrder( + IcalSemanticDocument.parse(object.rawIcs).components.single, + ); + final now = _now(); + final localId = 'dav-local-task-${_uuid.v4()}'; + final metadata = jsonEncode({ + 'transport': 'caldav', + 'uid': object.uid, + 'localPendingCreate': true, + }); await _database.transaction(() async { - final predecessor = await _latestPendingTaskEdit( - operation: operation, - taskListId: taskListId, - taskId: taskId, - ); - await _database.pendingOpsDao.enqueue( - PendingOpsCompanion.insert( - id: _uuid.v4(), + await _database.tasksDao.upsertTask( + TasksCompanion.insert( accountId: _accountId, - entityType: 'task', - operation: operation, - taskListId: Value(taskListId), - taskId: Value(taskId), - localTempId: Value(localTempId), - dependsOnOpId: Value(predecessor?.id), - requestJson: jsonEncode(request), - baselineUpdatedUtc: Value(baselineUpdatedUtc), - baselineRawJson: Value(baselineRawJson), - createdAtUtc: createdAtUtc, - updatedAtUtc: createdAtUtc, + taskListId: taskList.id, + id: localId, + davCollectionId: Value(collectionId), + icalUid: Value(object.uid), + parentUid: Value(parent?.icalUid), + sortOrder: Value(sortOrder), + title: input.title.trim(), + status: Value(fields['status']?.toString() ?? 'needsAction'), + parent: Value(parent?.id), + position: Value('$sortOrder'), + providerMetadataJson: Value(metadata), + rawJson: metadata, + localDirty: const Value(true), + localCreated: const Value(true), + createdLocalAtUtc: now, + updatedLocalAtUtc: now, ), ); + await _patchLocalTask(taskList.id, localId, fields, now); + await DavPendingOperationQueue( + database: _database, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ).enqueueCreate( + accountId: _accountId, + collectionId: collectionId, + object: object, + localProjectionId: localId, + ); }); + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); } - Future _latestPendingTaskEdit({ - required String operation, - required String? taskListId, - required String? taskId, + Future<({String localId, String uid, String lastOperationId})> + _duplicateDavTaskNode( + TaskList taskList, + Task source, { + required String? parentId, + required String? parentUid, + required String? dependsOnOperationId, + }) async { + await _ensureDavTaskMutable(taskList, source); + final collectionId = taskList.davCollectionId!; + final sourceUid = source.icalUid; + if (sourceUid == null || sourceUid.isEmpty) { + throw StateError('The DAV task UID is unavailable.'); + } + final queue = DavPendingOperationQueue( + database: _database, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ); + final sourceRaw = source.davObjectId == null + ? _pendingDavCreateRawIcs( + await _pendingDavCreateForProjection(source.id) ?? + (throw StateError('The pending DAV task is unavailable.')), + ) + : await queue.editableRawIcsForObject( + accountId: _accountId, + collectionId: collectionId, + objectId: source.davObjectId!, + ); + final newUid = _uuid.v4(); + final duplicatedRaw = _duplicateDavTaskResource( + sourceRaw, + sourceUid: sourceUid, + newUid: newUid, + parentUid: parentUid, + nowUtc: _nowUtc(), + ); + final duplicatedMaster = IcalSemanticDocument.parse( + duplicatedRaw, + ).components.singleWhere((component) => component.recurrenceIdKey == null); + final sortOrder = nextcloudTaskSortOrder(duplicatedMaster); + final localId = 'dav-local-task-${_uuid.v4()}'; + final metadata = jsonEncode({ + 'transport': 'caldav', + 'uid': newUid, + 'localPendingCreate': true, + 'duplicatedFromUid': sourceUid, + }); + final now = _now(); + late final String operationId; + await _database.transaction(() async { + await _database.tasksDao.upsertTask( + TasksCompanion.insert( + accountId: _accountId, + taskListId: taskList.id, + id: localId, + davCollectionId: Value(collectionId), + icalUid: Value(newUid), + icalPriority: Value(source.icalPriority), + percentComplete: Value(source.percentComplete), + taskLocation: Value(source.taskLocation), + taskUrl: Value(source.taskUrl), + taskClassification: Value(source.taskClassification), + taskPinned: Value(source.taskPinned), + taskHideSubtasks: Value(source.taskHideSubtasks), + taskHideCompletedSubtasks: Value(source.taskHideCompletedSubtasks), + taskAlarmsJson: Value(source.taskAlarmsJson), + parentUid: Value(parentUid), + sortOrder: Value(sortOrder), + title: source.title, + notes: Value(source.notes), + status: Value(source.status), + dueUtc: Value(source.dueUtc), + completedUtc: Value(source.completedUtc), + providerStatus: Value(source.providerStatus), + microsoftDueDateTime: Value(source.microsoftDueDateTime), + microsoftDueTimeZone: Value(source.microsoftDueTimeZone), + microsoftStartDateTime: Value(source.microsoftStartDateTime), + microsoftStartTimeZone: Value(source.microsoftStartTimeZone), + recurrenceJson: Value(source.recurrenceJson), + importance: Value(source.importance), + categoriesJson: Value(source.categoriesJson), + parent: Value(parentId), + position: Value('$sortOrder'), + providerMetadataJson: Value(metadata), + rawJson: metadata, + localDirty: const Value(true), + localCreated: const Value(true), + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); + operationId = await queue.enqueueCreate( + accountId: _accountId, + collectionId: collectionId, + object: DavNewObject( + uid: newUid, + initialMemberName: '${_uuid.v4()}.ics', + rawIcs: duplicatedRaw, + componentType: 'VTODO', + ), + localProjectionId: localId, + dependsOnOperationId: dependsOnOperationId, + ); + }); + + var lastOperationId = operationId; + for (final child in await _davChildren(taskList.id, source)) { + final duplicate = await _duplicateDavTaskNode( + taskList, + child, + parentId: localId, + parentUid: newUid, + dependsOnOperationId: lastOperationId, + ); + lastOperationId = duplicate.lastOperationId; + } + return (localId: localId, uid: newUid, lastOperationId: lastOperationId); + } + + Future _updateDavTask( + TaskList taskList, + String taskId, + Map requestedFields, { + bool announce = true, + }) async { + final task = await _requiredTask(taskList.id, taskId); + await _ensureDavTaskMutable( + taskList, + task, + changesClassification: requestedFields.containsKey('taskClassification'), + ); + if (task.recurrenceIdKey != null) { + throw UnsupportedError( + 'Editing an individual recurring DAV task occurrence is disabled.', + ); + } + final collectionId = taskList.davCollectionId!; + final uid = task.icalUid; + if (uid == null) throw StateError('The DAV task UID is unavailable.'); + final fields = Map.from(requestedFields); + String? parentUid; + if (fields.containsKey('parentUid')) { + final parent = await _davParent( + taskList.id, + fields['parentUid']?.toString(), + childTaskId: task.id, + ); + parentUid = parent?.icalUid; + fields['parentUid'] = parentUid; + } + final target = IcalComponentKey( + componentType: 'VTODO', + uid: uid, + recurrenceIdKey: task.recurrenceIdKey, + ); + final queue = DavPendingOperationQueue( + database: _database, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ); + final objectId = task.davObjectId; + late final String baselineRawIcs; + if (objectId == null) { + final create = await _pendingDavCreateForProjection(task.id); + if (create == null) { + throw StateError('The pending DAV task create is unavailable.'); + } + baselineRawIcs = _pendingDavCreateRawIcs(create); + } else { + baselineRawIcs = await queue.editableRawIcsForObject( + accountId: _accountId, + collectionId: collectionId, + objectId: objectId, + ); + } + final mutationNow = _nowUtc().toUtc(); + final patch = _buildDavTaskMutationPatch( + target: target, + baselineRawIcs: baselineRawIcs, + fields: fields, + parentUid: parentUid, + mutationNowUtc: mutationNow, + ); + if (patch == null) return; + final durablePatch = patch.materialize(mutationNow); + final candidate = durablePatch.applyTo(baselineRawIcs, nowUtc: mutationNow); + final now = mutationNow.toIso8601String(); + await _database.transaction(() async { + if (objectId == null) { + final updated = await queue.updateUnsentCreate( + accountId: _accountId, + collectionId: collectionId, + localProjectionId: task.id, + patch: durablePatch, + ); + if (!updated) { + throw StateError( + 'The pending DAV task create is no longer editable.', + ); + } + } else { + await queue.enqueueUpdate( + accountId: _accountId, + collectionId: collectionId, + objectId: objectId, + patch: durablePatch, + ); + } + if (objectId == null) { + await _patchLocalTask(taskList.id, task.id, fields, now); + } else { + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingle(); + await DavObjectRepository( + database: _database, + ).projectLocalMutationCandidate( + accountId: _accountId, + collectionId: collectionId, + provider: BusyProviderCodec.requireStorageValue(account.provider), + objectId: objectId, + candidateRawIcs: candidate, + projectedAtUtc: mutationNow, + ); + } + }); + if (announce) { + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + } + } + + Future _updateDavTaskWithHierarchy( + TaskList taskList, + String taskId, + Map fields, + ) async { + await _updateDavTaskHierarchyNode( + taskList, + taskId, + fields, + visited: {}, + ); + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + } + + Future _updateDavTaskHierarchyNode( + TaskList taskList, + String taskId, + Map fields, { + required Set visited, + }) async { + if (!visited.add(taskId)) { + throw StateError('The DAV task hierarchy contains a cycle.'); + } + final task = await _requiredTask(taskList.id, taskId); + try { + if (fields.containsKey('percentComplete')) { + final percent = fields['percentComplete']; + if (percent is! int || percent < 0 || percent > 100) { + throw ArgumentError.value(percent, 'percentComplete'); + } + if (percent < 100) { + final parent = await _davParentTask(taskList.id, task); + if (parent != null && _davTaskClosed(parent)) { + await _updateDavTaskHierarchyNode(taskList, parent.id, const { + 'percentComplete': 0, + }, visited: visited); + } + } else { + for (final child in await _davChildren(taskList.id, task)) { + if (!_davTaskClosed(child)) { + await _updateDavTaskHierarchyNode(taskList, child.id, const { + 'percentComplete': 100, + }, visited: visited); + } + } + } + } else if (fields.containsKey('taskStatus') || + fields.containsKey('status')) { + final requested = fields.containsKey('taskStatus') + ? fields['taskStatus'] + : fields['status']; + final status = _davProviderStatus(requested); + if (status != 'CANCELLED' && !_davTaskCompleted(task)) { + final parent = await _davParentTask(taskList.id, task); + if (parent != null && _davTaskClosed(parent)) { + await _updateDavTaskHierarchyNode(taskList, parent.id, const { + 'taskStatus': 'IN-PROCESS', + }, visited: visited); + } + } else { + for (final child in await _davChildren(taskList.id, task)) { + if (!_davTaskClosed(child)) { + await _updateDavTaskHierarchyNode(taskList, child.id, const { + 'taskStatus': 'CANCELLED', + }, visited: visited); + } + } + } + } + await _updateDavTask(taskList, task.id, fields, announce: false); + } finally { + visited.remove(taskId); + } + } + + Future _deleteDavTask( + TaskList taskList, + String taskId, { + String? dependsOnOperationId, + }) async { + final task = await _requiredTask(taskList.id, taskId); + await _ensureDavTaskMutable(taskList, task); + if (task.recurrenceIdKey != null) { + throw UnsupportedError( + 'Deleting an individual recurring DAV task occurrence is disabled.', + ); + } + final collectionId = taskList.davCollectionId!; + final uid = task.icalUid; + if (uid == null) throw StateError('The DAV task UID is unavailable.'); + final children = await _davChildren(taskList.id, task); + var dependency = dependsOnOperationId; + for (final child in children) { + dependency = await _deleteDavTask( + taskList, + child.id, + dependsOnOperationId: dependency, + ); + } + final queue = DavPendingOperationQueue( + database: _database, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ); + final objectId = task.davObjectId; + String? operationId; + await _database.transaction(() async { + if (objectId == null) { + final cancelled = await queue.cancelUnsentCreate( + accountId: _accountId, + collectionId: collectionId, + localProjectionId: task.id, + ); + if (!cancelled) { + throw StateError( + 'The DAV task create may already be in progress and cannot be ' + 'cancelled locally.', + ); + } + await (_database.delete(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskList.id) & + row.id.equals(task.id), + )) + .go(); + } else { + final object = await (_database.select( + _database.davObjects, + )..where((row) => row.id.equals(objectId))).getSingleOrNull(); + if (object == null || object.collectionId != collectionId) { + throw StateError('The DAV task baseline is unavailable.'); + } + final semantic = IcalSemanticDocument.parse(object.rawIcsBody); + final components = semantic.components + .where( + (component) => + component.componentType == 'VTODO' && component.uid == uid, + ) + .toList(growable: false); + if (components.isEmpty) { + throw StateError('The DAV task component is unavailable.'); + } + final targets = [ + for (final component in components) + IcalComponentKey( + componentType: component.componentType, + uid: uid, + recurrenceIdKey: component.recurrenceIdKey, + ), + ]; + final targetDocumentComponents = { + for (final component in components) component.documentComponent, + }; + final hasUntargetedCalendarComponent = semantic + .document + .calendarComponents + .any( + (component) => + component.name != 'VTIMEZONE' && + !targetDocumentComponents.contains(component), + ); + final scope = components.length > 1 + ? DavMutationScope.recurrenceMaster + : DavMutationScope.object; + if (!hasUntargetedCalendarComponent) { + operationId = await queue.enqueueDelete( + accountId: _accountId, + collectionId: collectionId, + objectId: objectId, + target: targets.first, + scope: scope, + dependsOnOperationId: dependency, + ); + await (_database.update( + _database.tasks, + )..where((row) => row.davObjectId.equals(objectId))).write( + TasksCompanion( + pendingDelete: const Value(true), + localDirty: const Value(true), + updatedLocalAtUtc: Value(_now()), + ), + ); + } else { + final patch = buildDavComponentRemovalPatch( + targets: targets, + scope: scope, + ); + final candidate = patch.applyTo(object.rawIcsBody, nowUtc: _nowUtc()); + operationId = await queue.enqueueUpdate( + accountId: _accountId, + collectionId: collectionId, + objectId: objectId, + patch: patch, + dependsOnOperationId: dependency, + ); + final account = await (_database.select( + _database.accounts, + )..where((row) => row.id.equals(_accountId))).getSingle(); + await DavObjectRepository( + database: _database, + ).projectLocalMutationCandidate( + accountId: _accountId, + collectionId: collectionId, + provider: BusyProviderCodec.requireStorageValue(account.provider), + objectId: objectId, + candidateRawIcs: candidate, + projectedAtUtc: _nowUtc(), + ); + } + } + }); + return operationId ?? dependency; + } + + Future _moveDavTask(TaskList taskList, TaskMoveInput input) async { + final destination = input.destinationTaskListId; + final task = await _requiredTask(taskList.id, input.taskId); + await _ensureDavTaskMutable(taskList, task); + if (task.recurrenceIdKey != null) { + throw UnsupportedError( + 'An individual recurring DAV task occurrence cannot be moved.', + ); + } + if (destination != null && destination != taskList.id) { + final destinationList = await _requiredTaskList(destination); + if (destinationList.davCollectionId == null) { + throw UnsupportedError( + 'Tasks cannot be moved between DAV and non-DAV task lists.', + ); + } + final parent = await _davParent( + destinationList.id, + input.parentTaskId, + childTaskId: null, + ); + await _ensureDavDestinationAllowsTask(destinationList, task); + await _moveDavTaskTreeToCollection( + sourceList: taskList, + destinationList: destinationList, + task: task, + parentId: parent?.id, + parentUid: parent?.icalUid, + completeSubtree: parent != null && _davTaskCompleted(parent), + dependsOnOperationId: null, + ); + return; + } + final parent = await _davParent( + taskList.id, + input.parentTaskId, + childTaskId: task.id, + ); + final ordering = await _davSortOrderForMove( + taskList.id, + parentId: parent?.id, + previousSiblingTaskId: input.previousSiblingTaskId, + movingTaskId: task.id, + ); + for (final adjustment in ordering.adjustments) { + await _updateDavTask(taskList, adjustment.taskId, { + 'sortOrder': adjustment.sortOrder, + }, announce: false); + } + final sortOrder = ordering.movingSortOrder; + final fields = { + 'parentUid': parent?.icalUid, + 'sortOrder': sortOrder, + }; + await _updateDavTask(taskList, task.id, fields, announce: false); + if (parent != null && + _davTaskCompleted(parent) && + !_davTaskCompleted(task)) { + await _updateDavTaskHierarchyNode(taskList, task.id, const { + 'percentComplete': 100, + }, visited: {}); + } + await _writeLocalTask( + taskList.id, + task.id, + TasksCompanion( + parent: Value(parent?.id), + parentUid: Value(parent?.icalUid), + sortOrder: Value(sortOrder), + position: Value('$sortOrder'), + pendingMove: const Value(true), + localDirty: const Value(true), + updatedLocalAtUtc: Value(_now()), + ), + ); + } + + Future _moveDavTaskTreeToCollection({ + required TaskList sourceList, + required TaskList destinationList, + required Task task, + required String? parentId, + required String? parentUid, + required bool completeSubtree, + required String? dependsOnOperationId, + }) async { + await _ensureDavTaskMutable(sourceList, task); + await _ensureDavDestinationAllowsTask(destinationList, task); + var dependency = dependsOnOperationId; + final children = await _davChildren(sourceList.id, task); + for (final child in children) { + dependency = await _moveDavTaskTreeToCollection( + sourceList: sourceList, + destinationList: destinationList, + task: child, + parentId: task.id, + parentUid: task.icalUid, + completeSubtree: completeSubtree, + dependsOnOperationId: dependency, + ); + } + + final queue = DavPendingOperationQueue( + database: _database, + idFactory: _uuid.v4, + nowUtc: _nowUtc, + ); + final fields = { + if (task.parentUid != parentUid) 'parentUid': parentUid, + if (completeSubtree && !_davTaskCompleted(task)) 'percentComplete': 100, + }; + final uid = task.icalUid; + if (uid == null || uid.isEmpty) { + throw StateError('The DAV task UID is unavailable.'); + } + final objectId = task.davObjectId; + late final String operationId; + if (objectId == null) { + final create = await _pendingDavCreateForProjection(task.id); + if (create == null) { + throw StateError('The pending DAV task create is unavailable.'); + } + final sourceObject = _pendingDavCreateObject(create); + var movedRaw = sourceObject.rawIcs; + if (fields.isNotEmpty) { + final patch = buildDavTaskUpdatePatch( + target: IcalComponentKey(componentType: 'VTODO', uid: uid), + baselineRawIcs: movedRaw, + fields: fields, + parentUid: parentUid, + nowUtc: _nowUtc, + ); + if (patch != null) { + movedRaw = patch.applyTo(movedRaw, nowUtc: _nowUtc().toUtc()); + } + } + final cancelled = await queue.cancelUnsentCreate( + accountId: _accountId, + collectionId: sourceList.davCollectionId!, + localProjectionId: task.id, + ); + if (!cancelled) { + throw StateError( + 'The pending DAV task create can no longer be moved locally.', + ); + } + operationId = await queue.enqueueCreate( + accountId: _accountId, + collectionId: destinationList.davCollectionId!, + object: DavNewObject( + uid: sourceObject.uid, + initialMemberName: sourceObject.initialMemberName, + rawIcs: movedRaw, + componentType: sourceObject.componentType, + ), + localProjectionId: task.id, + dependsOnOperationId: dependency, + ); + } else { + final editableRaw = await queue.editableRawIcsForObject( + accountId: _accountId, + collectionId: sourceList.davCollectionId!, + objectId: objectId, + ); + final postMovePatch = fields.isEmpty + ? null + : buildDavTaskUpdatePatch( + target: IcalComponentKey(componentType: 'VTODO', uid: uid), + baselineRawIcs: editableRaw, + fields: fields, + parentUid: parentUid, + nowUtc: _nowUtc, + ); + operationId = await queue.enqueueMove( + accountId: _accountId, + sourceCollectionId: sourceList.davCollectionId!, + destinationCollectionId: destinationList.davCollectionId!, + objectId: objectId, + target: IcalComponentKey(componentType: 'VTODO', uid: uid), + localProjectionId: task.id, + postMovePatch: postMovePatch, + dependsOnOperationId: dependency, + ); + } + + final now = _now(); + if (objectId == null) { + await _writeLocalTask( + sourceList.id, + task.id, + TasksCompanion( + taskListId: Value(destinationList.id), + davCollectionId: Value(destinationList.davCollectionId), + parent: Value(parentId), + parentUid: Value(parentUid), + status: completeSubtree + ? const Value('completed') + : const Value.absent(), + providerStatus: completeSubtree + ? const Value('COMPLETED') + : const Value.absent(), + percentComplete: completeSubtree + ? const Value(100) + : const Value.absent(), + completedUtc: completeSubtree + ? Value(task.completedUtc ?? now) + : const Value.absent(), + pendingMove: const Value(true), + localDirty: const Value(true), + updatedLocalAtUtc: Value(now), + ), + ); + } else { + await (_database.update(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.davObjectId.equals(objectId), + )) + .write( + TasksCompanion( + taskListId: Value(destinationList.id), + davCollectionId: Value(destinationList.davCollectionId), + parent: Value(parentId), + parentUid: Value(parentUid), + status: completeSubtree + ? const Value('completed') + : const Value.absent(), + providerStatus: completeSubtree + ? const Value('COMPLETED') + : const Value.absent(), + percentComplete: completeSubtree + ? const Value(100) + : const Value.absent(), + completedUtc: completeSubtree + ? Value(task.completedUtc ?? now) + : const Value.absent(), + pendingMove: const Value(true), + localDirty: const Value(true), + updatedLocalAtUtc: Value(now), + ), + ); + } + return operationId; + } + + Future _clearCompletedDavTasks(TaskList taskList) async { + final rows = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskList.id) & + row.pendingDelete.equals(false) & + row.serverMissing.equals(false), + )) + .get(); + final ids = {for (final task in rows) task.id}; + final uids = { + for (final task in rows) + if (task.icalUid case final uid?) uid, + }; + final roots = rows.where((task) { + if (task.recurrenceIdKey != null || !_davTaskClosed(task)) return false; + final parent = task.parent; + final parentUid = task.parentUid; + final parentAvailable = + (parent != null && ids.contains(parent)) || + (parentUid != null && uids.contains(parentUid)); + return !parentAvailable; + }); + final failures = []; + var queued = 0; + for (final task in roots) { + try { + await _deleteDavTask(taskList, task.id); + queued += 1; + } on Object { + failures.add(task.id); + } + } + if (queued > 0) { + await _rebuildTaskNotifications(); + _onMutationQueued?.call(); + } + if (failures.isNotEmpty) { + throw DavTaskBatchMutationException( + appliedCount: queued, + failedCount: failures.length, + ); + } + } + + Future _requiredTaskList(String taskListId) async { + final taskList = + await (_database.select(_database.taskLists)..where( + (row) => + row.accountId.equals(_accountId) & row.id.equals(taskListId), + )) + .getSingleOrNull(); + if (taskList == null || taskList.serverMissing || taskList.pendingDelete) { + throw StateError('The task list is unavailable.'); + } + return taskList; + } + + Future _requiredTask(String taskListId, String taskId) async { + final task = await _baselineRow(taskListId, taskId); + if (task == null || task.serverMissing || task.pendingDelete) { + throw StateError('The task is unavailable.'); + } + return task; + } + + Future _davParent( + String taskListId, + String? parentTaskId, { + required String? childTaskId, + }) async { + if (parentTaskId == null || parentTaskId.isEmpty) return null; + final rows = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.serverMissing.equals(false) & + row.pendingDelete.equals(false), + )) + .get(); + final parent = rows.firstWhereOrNull( + (row) => row.id == parentTaskId || row.icalUid == parentTaskId, + ); + if (parent == null || parent.icalUid == null) { + throw StateError('The DAV parent task is unavailable.'); + } + if (childTaskId != null && + _wouldCreateDavCycle(rows, childTaskId, parent)) { + throw ArgumentError('A task cannot be moved beneath its own subtree.'); + } + return parent; + } + + Future> _davChildren(String taskListId, Task task) { + return (_database.select(_database.tasks) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.pendingDelete.equals(false) & + row.serverMissing.equals(false) & + (row.parent.equals(task.id) | + (task.icalUid == null + ? const Constant(false) + : row.parentUid.equals(task.icalUid!))), + ) + ..orderBy([(row) => OrderingTerm.asc(row.sortOrder)])) + .get(); + } + + Future _davParentTask(String taskListId, Task task) async { + final parentId = task.parent; + final parentUid = task.parentUid; + if ((parentId == null || parentId.isEmpty) && + (parentUid == null || parentUid.isEmpty)) { + return null; + } + return (_database.select(_database.tasks) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.pendingDelete.equals(false) & + row.serverMissing.equals(false) & + ((parentId == null + ? const Constant(false) + : row.id.equals(parentId)) | + (parentUid == null + ? const Constant(false) + : row.icalUid.equals(parentUid))), + ) + ..limit(1)) + .getSingleOrNull(); + } + + Future _ensureDavCreateAllowed( + TaskList taskList, + Map fields, + ) async { + if (!await _davCollectionIsShared(taskList.davCollectionId!)) return; + final classification = _davClassification(fields['taskClassification']); + if (classification != null && classification != 'PUBLIC') { + throw UnsupportedError( + 'Private and confidential tasks cannot be created in a calendar ' + 'shared with this account.', + ); + } + } + + Future _ensureDavTaskMutable( + TaskList taskList, + Task task, { + bool changesClassification = false, + }) async { + if (!await _davCollectionIsShared(taskList.davCollectionId!)) return; + final classification = + task.taskClassification?.trim().toUpperCase() ?? 'PUBLIC'; + if (classification != 'PUBLIC') { + throw UnsupportedError( + 'Private and confidential tasks in a shared calendar are read-only.', + ); + } + if (changesClassification) { + throw UnsupportedError( + 'Task classification cannot be changed in a calendar shared with ' + 'this account.', + ); + } + } + + Future _ensureDavDestinationAllowsTask( + TaskList destination, + Task task, + ) async { + if (!await _davCollectionIsShared(destination.davCollectionId!)) return; + final classification = + task.taskClassification?.trim().toUpperCase() ?? 'PUBLIC'; + if (classification != 'PUBLIC') { + throw UnsupportedError( + 'Private and confidential tasks cannot be moved into a calendar ' + 'shared with this account.', + ); + } + } + + Future _davCollectionIsShared(String collectionId) async { + final collection = await (_database.select( + _database.davCollections, + )..where((row) => row.id.equals(collectionId))).getSingle(); + final service = await (_database.select( + _database.davAccountServices, + )..where((row) => row.accountId.equals(_accountId))).getSingleOrNull(); + final owner = _davHrefPath(collection.ownerHref); + final principal = _davHrefPath(service?.principalHref); + return owner != null && principal != null && owner != principal; + } + + Future< + ({int movingSortOrder, List<({String taskId, int sortOrder})> adjustments}) + > + _davSortOrderForMove( + String taskListId, { + required String? parentId, + required String? previousSiblingTaskId, + required String movingTaskId, + }) async { + final rows = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(movingTaskId).not() & + row.recurrenceIdKey.isNull() & + row.pendingDelete.equals(false) & + row.serverMissing.equals(false), + )) + .get(); + final siblings = rows.where((row) => row.parent == parentId).toList() + ..sort((left, right) { + final order = (left.sortOrder ?? 0).compareTo(right.sortOrder ?? 0); + return order != 0 ? order : left.id.compareTo(right.id); + }); + if (siblings.isEmpty) { + return ( + movingSortOrder: 0, + adjustments: const <({String taskId, int sortOrder})>[], + ); + } + final insertIndex = previousSiblingTaskId == null + ? 0 + : siblings.indexWhere((row) => row.id == previousSiblingTaskId) + 1; + if (previousSiblingTaskId != null && insertIndex == 0) { + throw ArgumentError('The previous DAV task sibling is unavailable.'); + } + final previous = insertIndex == 0 ? null : siblings[insertIndex - 1]; + final next = insertIndex < siblings.length ? siblings[insertIndex] : null; + final previousOrder = previous?.sortOrder ?? 0; + final nextOrder = next?.sortOrder ?? 0; + var proposedOrder = + next != null && (previous == null || nextOrder - 1 > previousOrder) + ? nextOrder - 1 + : previousOrder + 1; + if (proposedOrder < 0) proposedOrder = 0; + + final adjustments = <({String taskId, int sortOrder})>[]; + int? priorOrder; + var movingSortOrder = proposedOrder; + for (var index = 0; index <= siblings.length; index += 1) { + final moving = index == insertIndex; + final sibling = moving + ? null + : siblings[index < insertIndex ? index : index - 1]; + final currentOrder = moving ? proposedOrder : sibling!.sortOrder ?? 0; + final normalizedOrder = priorOrder != null && currentOrder <= priorOrder + ? priorOrder + 1 + : currentOrder; + if (moving) { + movingSortOrder = normalizedOrder; + } else if (normalizedOrder != currentOrder) { + adjustments.add((taskId: sibling!.id, sortOrder: normalizedOrder)); + } + priorOrder = normalizedOrder; + } + return ( + movingSortOrder: movingSortOrder, + adjustments: List<({String taskId, int sortOrder})>.unmodifiable( + adjustments, + ), + ); + } + + Future _pendingDavCreateForProjection(String projectionId) { + return (_database.select(_database.pendingOps) + ..where( + (row) => + row.taskId.equals(projectionId) & + row.operationType.equals('dav.create') & + row.state.isIn(const ['pending', 'failed']) & + row.attemptCount.equals(0), + ) + ..orderBy([(row) => OrderingTerm.asc(row.createdAtUtc)])) + .get() + .then( + (operations) => + operations.firstWhereOrNull(isDavCreateLocallyEditable), + ); + } + + Future refreshTask(String taskListId, String taskId) async { + final apiClient = _apiClient; + if (apiClient == null) { + return; + } + + final dto = await apiClient.getTask(taskListId: taskListId, taskId: taskId); + List? checklistItems; + if (apiClient is TaskChecklistRemoteClient) { + final checklistClient = apiClient as TaskChecklistRemoteClient; + final serverItems = []; + String? pageToken; + do { + final page = await checklistClient.listChecklistItemsPage( + taskListId: taskListId, + taskId: taskId, + pageToken: pageToken, + ); + serverItems.addAll(page.items); + pageToken = page.nextPageToken; + } while (pageToken != null && pageToken.isNotEmpty); + final localTask = await _baselineRow(taskListId, taskId); + final pending = await _pendingChecklistOperations(taskListId, taskId); + checklistItems = mergeTaskChecklistProjection( + serverItems: serverItems, + localItems: decodeTaskChecklistItems( + localTask?.microsoftChecklistItemsJson, + ), + pendingOperations: pending, + ); + } + final now = _now(); + await _database.transaction(() async { + await _database.tasksDao.upsertTask( + taskFromDto(_accountId, taskListId, dto, now), + ); + if (checklistItems != null) { + await _writeChecklistProjection( + taskListId, + taskId, + checklistItems, + now, + ); + } + }); + } + + Future _rebuildTaskNotifications() async { + await NotificationScheduleService( + database: _database, + nowUtc: _nowUtc, + ).rebuildUpcomingTaskNotifications(_accountId); + await _onNotificationScheduleChanged?.call(); + } + + Future _patchLocalTask( + String taskListId, + String taskId, + Map fields, + String now, + ) async { + final current = await _baselineRow(taskListId, taskId); + final davState = current?.davCollectionId == null + ? null + : _localDavTaskState(current!, fields, now); + await _writeLocalTask( + taskListId, + taskId, + TasksCompanion( + title: fields.containsKey('title') + ? Value(fields['title']?.toString() ?? '') + : const Value.absent(), + notes: fields.containsKey('notes') + ? Value(fields['notes']?.toString()) + : const Value.absent(), + status: davState != null + ? Value(davState.status) + : fields.containsKey('status') + ? Value(fields['status']?.toString()) + : const Value.absent(), + dueUtc: fields.containsKey('due') + ? Value(normalizeGoogleDueDateValue(fields['due'])) + : const Value.absent(), + completedUtc: davState != null + ? Value(davState.completedUtc) + : fields.containsKey('completed') + ? Value(fields['completed']?.toString()) + : fields.containsKey('status') + ? Value( + fields['status']?.toString().toLowerCase() == 'completed' + ? now + : null, + ) + : const Value.absent(), + providerStatus: davState != null + ? Value(davState.providerStatus) + : fields.containsKey('providerStatus') + ? Value(fields['providerStatus']?.toString()) + : fields.containsKey('status') + ? Value(_davProviderStatus(fields['status'])) + : const Value.absent(), + bodyContent: fields.containsKey('bodyContent') + ? Value(fields['bodyContent']?.toString()) + : const Value.absent(), + bodyContentType: fields.containsKey('bodyContentType') + ? Value(fields['bodyContentType']?.toString()) + : const Value.absent(), + microsoftDueDateTime: fields.containsKey('microsoftDueDateTime') + ? Value(_microsoftDateTimeField(fields['microsoftDueDateTime'])) + : const Value.absent(), + microsoftDueTimeZone: fields.containsKey('microsoftDueTimeZone') + ? Value(fields['microsoftDueTimeZone']?.toString()) + : const Value.absent(), + microsoftStartDateTime: fields.containsKey('microsoftStartDateTime') + ? Value(_microsoftDateTimeField(fields['microsoftStartDateTime'])) + : const Value.absent(), + microsoftStartTimeZone: fields.containsKey('microsoftStartTimeZone') + ? Value(fields['microsoftStartTimeZone']?.toString()) + : const Value.absent(), + microsoftReminderDateTime: + fields.containsKey('microsoftReminderDateTime') + ? Value( + _microsoftDateTimeField(fields['microsoftReminderDateTime']), + ) + : const Value.absent(), + microsoftReminderTimeZone: + fields.containsKey('microsoftReminderTimeZone') + ? Value(fields['microsoftReminderTimeZone']?.toString()) + : const Value.absent(), + microsoftIsReminderOn: fields.containsKey('microsoftIsReminderOn') + ? Value(fields['microsoftIsReminderOn'] as bool?) + : const Value.absent(), + recurrenceJson: fields.containsKey('recurrence') + ? Value(_jsonOrNull(fields['recurrence'])) + : const Value.absent(), + importance: fields.containsKey('importance') + ? Value(fields['importance']?.toString()) + : const Value.absent(), + icalPriority: fields.containsKey('icalPriority') + ? Value(_exactDavPriority(fields['icalPriority'])) + : fields.containsKey('importance') + ? Value(_davPriority(fields['importance'])) + : const Value.absent(), + percentComplete: davState != null + ? Value(davState.percentComplete) + : fields.containsKey('percentComplete') || + fields.containsKey('status') + ? Value(_davPercentComplete(fields)) + : const Value.absent(), + taskLocation: fields.containsKey('location') + ? Value(_trimmedOrNull(fields['location'])) + : const Value.absent(), + taskUrl: fields.containsKey('taskUrl') + ? Value(_trimmedOrNull(fields['taskUrl'])) + : const Value.absent(), + taskClassification: fields.containsKey('taskClassification') + ? Value(_davClassification(fields['taskClassification'])) + : const Value.absent(), + taskPinned: fields.containsKey('taskPinned') + ? Value(fields['taskPinned'] == true) + : const Value.absent(), + taskHideSubtasks: fields.containsKey('taskHideSubtasks') + ? Value(fields['taskHideSubtasks'] == true) + : const Value.absent(), + taskHideCompletedSubtasks: + fields.containsKey('taskHideCompletedSubtasks') + ? Value(fields['taskHideCompletedSubtasks'] == true) + : const Value.absent(), + taskAlarmsJson: fields.containsKey('taskAlarms') + ? Value(jsonEncode(fields['taskAlarms'])) + : const Value.absent(), + parentUid: fields.containsKey('parentUid') + ? Value(fields['parentUid']?.toString()) + : const Value.absent(), + sortOrder: fields.containsKey('sortOrder') + ? Value(fields['sortOrder'] as int?) + : const Value.absent(), + categoriesJson: fields.containsKey('categories') + ? Value(_jsonOrNull(fields['categories'])) + : const Value.absent(), + deleted: fields.containsKey('deleted') + ? Value(fields['deleted'] as bool?) + : const Value.absent(), + localDirty: const Value(true), + updatedLocalAtUtc: Value(now), + ), + ); + } + + Future _writeLocalTask( + String taskListId, + String taskId, + TasksCompanion companion, + ) { + final update = _database.update(_database.tasks) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.taskListId.equals(taskListId) & + row.id.equals(taskId), + ); + return update.write(companion); + } + + Future _writeChecklistProjection( + String taskListId, + String parentTaskId, + Iterable items, + String now, + ) { + return _writeLocalTask( + taskListId, + parentTaskId, + TasksCompanion( + microsoftChecklistItemsJson: Value(encodeTaskChecklistItems(items)), + updatedLocalAtUtc: Value(now), + ), + ); + } + + Future _enqueueChecklistOperation({ + required String operation, + required String taskListId, + required String parentTaskId, + required String checklistItemId, + required Map request, + required String createdAtUtc, + String? localTempId, + }) async { + final predecessor = await _latestPendingChecklistOperation( + taskListId, + parentTaskId, + checklistItemId, + ); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: _uuid.v4(), + accountId: _accountId, + entityType: 'task_checklist_item', + operation: operation, + taskListId: Value(taskListId), + taskId: Value(parentTaskId), + localTempId: Value(localTempId), + dependsOnOpId: Value(predecessor?.id), + requestJson: jsonEncode(request), + createdAtUtc: createdAtUtc, + updatedAtUtc: createdAtUtc, + ), + ); + } + + Future> _pendingChecklistOperations( + String taskListId, + String parentTaskId, + ) { + final query = _database.select(_database.pendingOps) + ..where( + (row) => + row.accountId.equals(_accountId) & + row.entityType.equals('task_checklist_item') & + row.taskListId.equals(taskListId) & + row.taskId.equals(parentTaskId), + ) + ..orderBy([ + (row) => OrderingTerm.asc(row.createdAtUtc), + (row) => OrderingTerm.asc(row.updatedAtUtc), + ]); + return query.get(); + } + + Future _latestPendingChecklistOperation( + String taskListId, + String parentTaskId, + String checklistItemId, + ) async { + final operations = await _pendingChecklistOperations( + taskListId, + parentTaskId, + ); + for (final operation in operations.reversed) { + if (_checklistItemIdFromOperation(operation) == checklistItemId) { + return operation; + } + } + return null; + } + + Future _pendingChecklistCreate( + String taskListId, + String parentTaskId, + String checklistItemId, + ) async { + final operations = await _pendingChecklistOperations( + taskListId, + parentTaskId, + ); + return operations.firstWhereOrNull( + (operation) => + operation.operation == 'create_task_checklist_item' && + _checklistItemIdFromOperation(operation) == checklistItemId, + ); + } + + Future _deleteChecklistOperationChain({ + required String parentTaskId, + required String checklistItemId, + }) async { + final operations = + await (_database.select(_database.pendingOps)..where( + (row) => + row.accountId.equals(_accountId) & + row.entityType.equals('task_checklist_item') & + row.taskId.equals(parentTaskId), + )) + .get(); + for (final operation in operations) { + if (_checklistItemIdFromOperation(operation) == checklistItemId) { + await _database.pendingOpsDao.deleteOp(operation.id); + } + } + } + + Future _enqueue({ + required String operation, + required Map request, + required String createdAtUtc, + String? taskListId, + String? taskId, + String? localTempId, + String? baselineUpdatedUtc, + String? baselineRawJson, + String? dependsOnOpId, + }) async { + final operationId = _uuid.v4(); + await _database.transaction(() async { + final predecessor = await _latestPendingTaskEdit( + operation: operation, + taskListId: taskListId, + taskId: taskId, + ); + await _database.pendingOpsDao.enqueue( + PendingOpsCompanion.insert( + id: operationId, + accountId: _accountId, + entityType: 'task', + operation: operation, + taskListId: Value(taskListId), + taskId: Value(taskId), + localTempId: Value(localTempId), + dependsOnOpId: Value(dependsOnOpId ?? predecessor?.id), + requestJson: jsonEncode(request), + baselineUpdatedUtc: Value(baselineUpdatedUtc), + baselineRawJson: Value(baselineRawJson), + createdAtUtc: createdAtUtc, + updatedAtUtc: createdAtUtc, + ), + ); + }); + return operationId; + } + + Future _latestPendingTaskEdit({ + required String operation, + required String? taskListId, + required String? taskId, }) async { if ((operation != 'patch_task' && operation != 'update_task') || taskListId == null || @@ -773,21 +2470,64 @@ class TasksRepository { } List _buildTree(List tasks) { - final byParent = groupBy(tasks, (task) => task.parent ?? ''); + final byId = {for (final task in tasks) task.id: task}; + final byUid = { + for (final task in tasks) + if (task.icalUid != null && task.icalUid!.isNotEmpty) + task.icalUid!: task, + }; + TaskEntity? parentOf(TaskEntity task) { + final parentId = task.parent; + final parentUid = task.parentUid; + return (parentId == null ? null : byId[parentId] ?? byUid[parentId]) ?? + (parentUid == null ? null : byUid[parentUid] ?? byId[parentUid]); + } - List buildChildren(String parentId) { - final children = [...byParent[parentId] ?? const []] - ..sort(_compareTaskOrder); - return [ - for (final child in children) - TaskTreeNode(task: child, children: buildChildren(child.id)), - ]; + final byParent = >{}; + final roots = []; + for (final task in tasks) { + final parent = parentOf(task); + if (parent == null || parent.id == task.id) { + roots.add(task); + } else { + byParent.putIfAbsent(parent.id, () => []).add(task); + } + } + roots.sort(_compareTaskOrder); + for (final children in byParent.values) { + children.sort(_compareTaskOrder); + } + + final emitted = {}; + TaskTreeNode buildNode(TaskEntity task, Set ancestors) { + emitted.add(task.id); + if (!ancestors.add(task.id)) { + return TaskTreeNode(task: task, children: const []); + } + final children = []; + for (final child in byParent[task.id] ?? const []) { + if (!ancestors.contains(child.id)) { + children.add(buildNode(child, ancestors)); + } + } + ancestors.remove(task.id); + return TaskTreeNode(task: task, children: children); } - return buildChildren(''); + final nodes = [for (final root in roots) buildNode(root, {})]; + for (final task in tasks..sort(_compareTaskOrder)) { + if (!emitted.contains(task.id)) { + nodes.add(buildNode(task, {})); + } + } + return nodes; } int _compareTaskOrder(TaskEntity left, TaskEntity right) { + if (left.sortOrder != null && right.sortOrder != null) { + final sortOrderCompare = left.sortOrder!.compareTo(right.sortOrder!); + if (sortOrderCompare != 0) return sortOrderCompare; + } final positionCompare = (left.position ?? '').compareTo( right.position ?? '', ); @@ -800,6 +2540,498 @@ class TasksRepository { String _now() => _nowUtc().toIso8601String(); } +TaskChecklistItemEntity taskChecklistItemFromDto(TaskChecklistItemDto dto) { + final raw = { + ...dto.rawJson, + 'id': dto.id, + 'displayName': dto.title, + 'isChecked': dto.completed, + if (dto.createdAtUtc != null) + 'createdDateTime': dto.createdAtUtc!.toIso8601String(), + if (dto.completedAtUtc != null) + 'checkedDateTime': dto.completedAtUtc!.toIso8601String(), + }; + return TaskChecklistItemEntity.fromJson(raw); +} + +List mergeTaskChecklistProjection({ + required Iterable serverItems, + required Iterable localItems, + required Iterable pendingOperations, +}) { + final items = { + for (final item in serverItems.map(taskChecklistItemFromDto)) item.id: item, + }; + final localById = {for (final item in localItems) item.id: item}; + for (final operation in pendingOperations) { + final itemId = _checklistItemIdFromOperation(operation); + if (itemId == null) continue; + final request = _pendingRequestOrNull(operation); + if (request == null) continue; + switch (operation.operation) { + case 'create_task_checklist_item': + final local = localById[itemId]; + if (local != null) { + items[itemId] = local; + continue; + } + final body = request['body']; + if (body is Map) { + items[itemId] = TaskChecklistItemEntity.fromJson({ + ...body.cast(), + 'id': itemId, + }); + } + case 'patch_task_checklist_item': + final current = items[itemId]; + final body = request['body']; + if (current == null || body is! Map) continue; + items[itemId] = TaskChecklistItemEntity.fromJson({ + ...current.toJson(), + ...body.cast(), + }); + case 'delete_task_checklist_item': + items.remove(itemId); + } + } + return List.unmodifiable(items.values); +} + +String? _checklistItemIdFromOperation(PendingOp operation) { + final request = _pendingRequestOrNull(operation); + return request?['checklistItemId']?.toString() ?? operation.localTempId; +} + +Map? _pendingRequestOrNull(PendingOp operation) { + try { + final decoded = jsonDecode(operation.requestJson); + return decoded is Map ? decoded.cast() : null; + } on FormatException { + return null; + } +} + +String _duplicateDavTaskResource( + String sourceRawIcs, { + required String sourceUid, + required String newUid, + required String? parentUid, + required DateTime nowUtc, +}) { + final semantic = IcalSemanticDocument.parse(sourceRawIcs); + final sourceComponents = semantic.components + .where( + (component) => + component.componentType == 'VTODO' && component.uid == sourceUid, + ) + .toList(growable: false); + if (sourceComponents.isEmpty || + sourceComponents + .where((component) => component.recurrenceIdKey == null) + .length != + 1) { + throw StateError('The DAV task recurrence set is unavailable.'); + } + + // RFC 5545 requires every component in a recurrence set to share its UID. + // Nextcloud changes the duplicated task's UID; update detached instances as + // well so a recurring duplicate remains one valid calendar object resource. + final patcher = IcalDocumentPatcher(semantic.document); + for (final component in sourceComponents) { + patcher.replaceSingletonRaw( + IcalComponentKey( + componentType: 'VTODO', + uid: sourceUid, + recurrenceIdKey: component.recurrenceIdKey, + ), + 'UID', + newUid, + ); + } + + final timestamp = _utcIcalTimestamp(nowUtc); + final duplicated = DavMutationPatch( + target: IcalComponentKey(componentType: 'VTODO', uid: newUid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setRaw('CREATED', timestamp), + DavPatchOperation.setRaw('LAST-MODIFIED', timestamp), + DavPatchOperation.setRaw('DTSTAMP', timestamp), + DavPatchOperation.setTaskParent(parentUid), + ], + ).applyTo(semantic.document.serialize(), nowUtc: nowUtc.toUtc()); + final result = IcalSemanticDocument.parse(duplicated); + if (result.primaryUid != newUid || + result.components.any( + (component) => + component.componentType == 'VTODO' && component.uid != newUid, + )) { + throw StateError('The duplicated DAV task is invalid.'); + } + return duplicated; +} + +String _utcIcalTimestamp(DateTime value) { + final utc = value.toUtc(); + String two(int part) => part.toString().padLeft(2, '0'); + return '${utc.year.toString().padLeft(4, '0')}' + '${two(utc.month)}${two(utc.day)}T' + '${two(utc.hour)}${two(utc.minute)}${two(utc.second)}Z'; +} + +bool _wouldCreateDavCycle( + List tasks, + String childTaskId, + Task proposedParent, +) { + final byId = {for (final task in tasks) task.id: task}; + final byUid = { + for (final task in tasks) + if (task.icalUid != null) task.icalUid!: task, + }; + final seen = {}; + Task? cursor = proposedParent; + while (cursor != null && seen.add(cursor.id)) { + if (cursor.id == childTaskId) return true; + final parentKey = cursor.parentUid ?? cursor.parent; + cursor = parentKey == null ? null : byId[parentKey] ?? byUid[parentKey]; + } + return false; +} + +String? _davHrefPath(String? source) { + final value = source?.trim(); + if (value == null || value.isEmpty) return null; + final uri = Uri.tryParse(value); + if (uri == null) return null; + var path = uri.path; + while (path.length > 1 && path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + return path; +} + +bool _davTaskCompleted(Task task) => + task.providerStatus?.toUpperCase() == 'COMPLETED' || + task.completedUtc != null; + +bool _davTaskClosed(Task task) => + _davTaskCompleted(task) || + task.providerStatus?.toUpperCase() == 'CANCELLED'; + +DavMutationPatch? _buildDavTaskMutationPatch({ + required IcalComponentKey target, + required String baselineRawIcs, + required Map fields, + required DateTime mutationNowUtc, + String? parentUid, +}) { + DavMutationPatch? regular(Map values) => + buildDavTaskUpdatePatch( + target: target, + baselineRawIcs: baselineRawIcs, + fields: values, + parentUid: parentUid, + nowUtc: () => mutationNowUtc, + ); + + if (!_requestsDavTaskCompletion(fields)) return regular(fields); + final nonCompletionFields = Map.from(fields) + ..remove('status') + ..remove('taskStatus') + ..remove('percentComplete') + ..remove('completedAtUtc') + ..remove('completed'); + final preceding = regular(nonCompletionFields); + final preparedRawIcs = preceding == null + ? baselineRawIcs + : preceding.applyTo(baselineRawIcs, nowUtc: mutationNowUtc); + final prepared = IcalSemanticDocument.parse(preparedRawIcs); + final master = prepared.components.firstWhereOrNull( + (component) => + component.componentType == 'VTODO' && + component.uid == target.uid && + component.recurrenceIdKey == null, + ); + if (master == null || + master.recurrenceRules.isEmpty || + master.taskUiState == IcalTaskUiState.completed) { + return regular(fields); + } + final completedAt = _requestedDavCompletionDate(fields); + final completion = buildDavRecurringTaskCompletionPatch( + target: target, + baselineRawIcs: preparedRawIcs, + completedAtUtc: completedAt, + nowUtc: () => mutationNowUtc, + ); + if (preceding == null) return completion; + return DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [...preceding.operations, ...completion.operations], + ); +} + +bool _requestsDavTaskCompletion(Map fields) { + if (fields['completedAtUtc'] != null || fields['completed'] != null) { + return true; + } + if (fields['percentComplete'] == 100) return true; + final value = fields.containsKey('taskStatus') + ? fields['taskStatus'] + : fields['status']; + return switch (value?.toString().trim().toUpperCase()) { + 'COMPLETED' => true, + _ => false, + }; +} + +DateTime? _requestedDavCompletionDate(Map fields) { + final value = fields['completedAtUtc'] ?? fields['completed']; + if (value == null) return null; + final parsed = value is DateTime + ? value + : DateTime.tryParse(value.toString()); + if (parsed == null) throw ArgumentError.value(value, 'completedAtUtc'); + return parsed.toUtc(); +} + +String _pendingDavCreateRawIcs(PendingOp operation) { + return _pendingDavCreateObject(operation).rawIcs; +} + +DavNewObject _pendingDavCreateObject(PendingOp operation) { + try { + final decoded = jsonDecode(operation.requestJson); + if (decoded is Map && + decoded['uid'] is String && + decoded['initialMemberName'] is String && + decoded['rawIcs'] is String && + decoded['componentType'] is String) { + return DavNewObject( + uid: decoded['uid']! as String, + initialMemberName: decoded['initialMemberName']! as String, + rawIcs: decoded['rawIcs']! as String, + componentType: decoded['componentType']! as String, + ); + } + } on FormatException { + // Invalid pending payloads use the same local-state error. + } + throw StateError('The pending DAV task body is invalid.'); +} + +int? _davPriority(Object? importance) => switch (importance?.toString()) { + 'high' => 1, + 'low' => 9, + 'normal' || null => null, + _ => null, +}; + +int? _exactDavPriority(Object? value) { + if (value == null) return null; + if (value is! int || value < 0 || value > 9) { + throw ArgumentError.value(value, 'icalPriority'); + } + return value == 0 ? null : value; +} + +({ + String status, + String? providerStatus, + int? percentComplete, + String? completedUtc, +})? +_localDavTaskState(Task current, Map fields, String now) { + final hasStatus = + fields.containsKey('taskStatus') || fields.containsKey('status'); + final hasPercent = fields.containsKey('percentComplete'); + final hasCompleted = fields.containsKey('completedAtUtc'); + if (!hasStatus && !hasPercent && !hasCompleted) return null; + + final currentPercent = current.percentComplete ?? 0; + final completedValue = fields['completedAtUtc']; + if (hasCompleted && completedValue != null) { + final parsed = DateTime.tryParse(completedValue.toString()); + if (parsed == null) throw ArgumentError.value(completedValue); + return ( + status: 'completed', + providerStatus: 'COMPLETED', + percentComplete: 100, + completedUtc: parsed.toUtc().toIso8601String(), + ); + } + if (hasPercent) { + final percent = fields['percentComplete']; + if (percent is! int || percent < 0 || percent > 100) { + throw ArgumentError.value(percent, 'percentComplete'); + } + if (percent == 100) { + return ( + status: 'completed', + providerStatus: 'COMPLETED', + percentComplete: 100, + completedUtc: current.completedUtc ?? now, + ); + } + if (percent == 0) { + return ( + status: 'needsAction', + providerStatus: 'NEEDS-ACTION', + percentComplete: null, + completedUtc: null, + ); + } + return ( + status: 'inProcess', + providerStatus: 'IN-PROCESS', + percentComplete: percent, + completedUtc: null, + ); + } + if (hasStatus) { + final raw = fields.containsKey('taskStatus') + ? fields['taskStatus'] + : fields['status']; + final providerStatus = _davProviderStatus(raw); + return switch (providerStatus) { + 'COMPLETED' => ( + status: 'completed', + providerStatus: 'COMPLETED', + percentComplete: 100, + completedUtc: current.completedUtc ?? now, + ), + 'IN-PROCESS' => ( + status: 'inProcess', + providerStatus: 'IN-PROCESS', + percentComplete: currentPercent == 100 + ? 99 + : currentPercent == 0 + ? 1 + : currentPercent, + completedUtc: null, + ), + 'CANCELLED' => ( + status: 'cancelled', + providerStatus: 'CANCELLED', + percentComplete: current.percentComplete, + completedUtc: current.completedUtc, + ), + 'NEEDS-ACTION' || null => ( + status: 'needsAction', + providerStatus: providerStatus, + percentComplete: currentPercent == 100 ? 99 : current.percentComplete, + completedUtc: null, + ), + _ => throw ArgumentError.value(raw, 'taskStatus'), + }; + } + if (currentPercent == 100) { + return ( + status: 'inProcess', + providerStatus: 'IN-PROCESS', + percentComplete: 99, + completedUtc: null, + ); + } + return ( + status: current.status ?? 'needsAction', + providerStatus: current.providerStatus, + percentComplete: current.percentComplete, + completedUtc: null, + ); +} + +String? _trimmedOrNull(Object? value) { + final text = value?.toString().trim(); + return text == null || text.isEmpty ? null : text; +} + +String? _davClassification(Object? value) => + switch (value?.toString().toUpperCase()) { + 'PUBLIC' => 'PUBLIC', + 'PRIVATE' => 'PRIVATE', + 'CONFIDENTIAL' => 'CONFIDENTIAL', + null || '' => null, + _ => throw ArgumentError.value(value, 'taskClassification'), + }; + +int _davPercentComplete(Map fields) { + final explicit = fields['percentComplete']; + if (explicit is int) return explicit.clamp(0, 100); + return switch (fields['status']?.toString().toLowerCase()) { + 'completed' => 100, + 'inprocess' || 'in-process' => 50, + _ => 0, + }; +} + +String? _davProviderStatus(Object? status) => + switch (status?.toString().toLowerCase()) { + 'completed' => 'COMPLETED', + 'inprocess' || 'in-process' => 'IN-PROCESS', + 'needsaction' || 'needs-action' => 'NEEDS-ACTION', + null => null, + final value => value.toUpperCase(), + }; + +({ + String? dueDateTime, + String? dueTimeZone, + String? startDateTime, + String? startTimeZone, +}) +_davNativeTaskFields(String? source) { + const empty = ( + dueDateTime: null, + dueTimeZone: null, + startDateTime: null, + startTimeZone: null, + ); + if (source == null || source.isEmpty) return empty; + try { + final decoded = jsonDecode(source); + if (decoded is! Map) return empty; + final due = _davNativeTemporal(decoded['nativeDue']); + final start = _davNativeTemporal(decoded['nativeStart']); + return ( + dueDateTime: due.dateTime, + dueTimeZone: due.timeZone, + startDateTime: start.dateTime, + startTimeZone: start.timeZone, + ); + } on FormatException { + return empty; + } +} + +({String? dateTime, String? timeZone}) _davNativeTemporal(Object? source) { + if (source is! Map) return (dateTime: null, timeZone: null); + final raw = source['raw']?.toString(); + if (raw == null || raw.isEmpty) return (dateTime: null, timeZone: null); + final kind = source['kind']?.toString(); + final date = RegExp(r'^(\d{4})(\d{2})(\d{2})$').firstMatch(raw); + if (date != null) { + return ( + dateTime: '${date.group(1)}-${date.group(2)}-${date.group(3)}', + timeZone: null, + ); + } + final dateTime = RegExp( + r'^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)$', + ).firstMatch(raw); + if (dateTime == null) return (dateTime: null, timeZone: null); + final normalized = + '${dateTime.group(1)}-${dateTime.group(2)}-${dateTime.group(3)}' + 'T${dateTime.group(4)}:${dateTime.group(5)}:${dateTime.group(6)}' + '${dateTime.group(7)}'; + return ( + dateTime: normalized, + timeZone: kind == 'utcDateTime' ? 'UTC' : source['timeZoneId']?.toString(), + ); +} + String _accountLabel(Account account) { final name = account.displayName?.trim(); if (name != null && name.isNotEmpty) { @@ -809,7 +3041,7 @@ String _accountLabel(Account account) { if (address != null && address.isNotEmpty) { return address; } - return TaskProviderParsing.fromStorageValue(account.provider).displayName; + return BusyProviderCodec.requireStorageValue(account.provider).displayName; } Map _remoteTaskFields(Map fields) { @@ -900,6 +3132,9 @@ TasksCompanion taskFromDto( microsoftCompletedTimeZone: Value( _dateTimeTimeZoneTimeZone(dto.rawJson['completedDateTime']), ), + microsoftChecklistItemsJson: dto.rawJson.containsKey('checklistItems') + ? Value(_jsonOrNull(dto.rawJson['checklistItems'])) + : const Value.absent(), recurrenceJson: Value(_jsonOrNull(dto.rawJson['recurrence'])), importance: Value(_stringOrNull(dto.rawJson['importance'])), categoriesJson: Value(_jsonOrNull(dto.rawJson['categories'])), diff --git a/lib/src/features/tasks/domain/task_capabilities.dart b/lib/src/features/tasks/domain/task_capabilities.dart new file mode 100644 index 0000000..a141105 --- /dev/null +++ b/lib/src/features/tasks/domain/task_capabilities.dart @@ -0,0 +1,243 @@ +import '../../../providers/busy_provider.dart'; + +/// Task operations available for a concrete list or DAV collection. +class TaskCollectionCapabilities { + const TaskCollectionCapabilities({ + required this.supportsDueDate, + required this.supportsDueTime, + required this.supportsStartDateTime, + required this.supportsReminderDateTime, + required this.supportsRecurrence, + required this.supportsImportance, + required this.supportsCategories, + required this.supportsTaskHierarchy, + required this.supportsCrossListMove, + required this.supportsClearCompleted, + required this.supportsHiddenTasks, + required this.supportsAssignedTasks, + required this.supportsListRename, + required this.supportsListDelete, + this.canCreateTasks = true, + this.canUpdateTasks = true, + this.canDeleteTasks = true, + this.supportsRecurringTaskOccurrenceEditing = false, + this.supportsIcalPriority = false, + this.supportsPercentComplete = false, + this.supportsTaskStatus = false, + this.supportsCompletedDateTime = false, + this.supportsLocation = false, + this.supportsUrl = false, + this.supportsClassification = false, + this.supportsMultipleReminders = false, + this.supportsAdvancedRecurrence = false, + this.supportsPinning = false, + this.supportsSubtaskVisibility = false, + this.supportsDuplicate = false, + this.supportsNativeExport = false, + this.supportsTaskReparenting = false, + this.canUpdateClassification = true, + }); + + final bool supportsDueDate; + final bool supportsDueTime; + final bool supportsStartDateTime; + final bool supportsReminderDateTime; + final bool supportsRecurrence; + final bool supportsImportance; + final bool supportsCategories; + final bool supportsTaskHierarchy; + final bool supportsCrossListMove; + final bool supportsClearCompleted; + final bool supportsHiddenTasks; + final bool supportsAssignedTasks; + final bool supportsListRename; + final bool supportsListDelete; + final bool canCreateTasks; + final bool canUpdateTasks; + final bool canDeleteTasks; + final bool supportsRecurringTaskOccurrenceEditing; + final bool supportsIcalPriority; + final bool supportsPercentComplete; + final bool supportsTaskStatus; + final bool supportsCompletedDateTime; + final bool supportsLocation; + final bool supportsUrl; + final bool supportsClassification; + final bool supportsMultipleReminders; + final bool supportsAdvancedRecurrence; + final bool supportsPinning; + final bool supportsSubtaskVisibility; + final bool supportsDuplicate; + final bool supportsNativeExport; + final bool supportsTaskReparenting; + final bool canUpdateClassification; + + TaskCollectionCapabilities asReadOnly() => TaskCollectionCapabilities( + supportsDueDate: supportsDueDate, + supportsDueTime: supportsDueTime, + supportsStartDateTime: supportsStartDateTime, + supportsReminderDateTime: supportsReminderDateTime, + supportsRecurrence: supportsRecurrence, + supportsImportance: supportsImportance, + supportsCategories: supportsCategories, + supportsTaskHierarchy: supportsTaskHierarchy, + supportsCrossListMove: false, + supportsClearCompleted: false, + supportsHiddenTasks: supportsHiddenTasks, + supportsAssignedTasks: supportsAssignedTasks, + supportsListRename: false, + supportsListDelete: false, + canCreateTasks: false, + canUpdateTasks: false, + canDeleteTasks: false, + supportsIcalPriority: supportsIcalPriority, + supportsPercentComplete: supportsPercentComplete, + supportsTaskStatus: supportsTaskStatus, + supportsCompletedDateTime: supportsCompletedDateTime, + supportsLocation: supportsLocation, + supportsUrl: supportsUrl, + supportsClassification: supportsClassification, + supportsMultipleReminders: supportsMultipleReminders, + supportsAdvancedRecurrence: supportsAdvancedRecurrence, + supportsPinning: supportsPinning, + supportsSubtaskVisibility: supportsSubtaskVisibility, + supportsDuplicate: supportsDuplicate, + supportsNativeExport: supportsNativeExport, + supportsTaskReparenting: supportsTaskReparenting, + canUpdateClassification: false, + ); + + TaskCollectionCapabilities withoutClassificationEditing() => + TaskCollectionCapabilities( + supportsDueDate: supportsDueDate, + supportsDueTime: supportsDueTime, + supportsStartDateTime: supportsStartDateTime, + supportsReminderDateTime: supportsReminderDateTime, + supportsRecurrence: supportsRecurrence, + supportsImportance: supportsImportance, + supportsCategories: supportsCategories, + supportsTaskHierarchy: supportsTaskHierarchy, + supportsCrossListMove: supportsCrossListMove, + supportsClearCompleted: supportsClearCompleted, + supportsHiddenTasks: supportsHiddenTasks, + supportsAssignedTasks: supportsAssignedTasks, + supportsListRename: supportsListRename, + supportsListDelete: supportsListDelete, + canCreateTasks: canCreateTasks, + canUpdateTasks: canUpdateTasks, + canDeleteTasks: canDeleteTasks, + supportsRecurringTaskOccurrenceEditing: + supportsRecurringTaskOccurrenceEditing, + supportsIcalPriority: supportsIcalPriority, + supportsPercentComplete: supportsPercentComplete, + supportsTaskStatus: supportsTaskStatus, + supportsCompletedDateTime: supportsCompletedDateTime, + supportsLocation: supportsLocation, + supportsUrl: supportsUrl, + supportsClassification: supportsClassification, + supportsMultipleReminders: supportsMultipleReminders, + supportsAdvancedRecurrence: supportsAdvancedRecurrence, + supportsPinning: supportsPinning, + supportsSubtaskVisibility: supportsSubtaskVisibility, + supportsDuplicate: supportsDuplicate, + supportsNativeExport: supportsNativeExport, + supportsTaskReparenting: supportsTaskReparenting, + canUpdateClassification: false, + ); +} + +const googleTaskCollectionCapabilities = TaskCollectionCapabilities( + supportsDueDate: true, + supportsDueTime: false, + supportsStartDateTime: false, + supportsReminderDateTime: false, + supportsRecurrence: false, + supportsImportance: false, + supportsCategories: false, + supportsTaskHierarchy: true, + supportsCrossListMove: true, + supportsClearCompleted: true, + supportsHiddenTasks: true, + supportsAssignedTasks: true, + supportsListRename: true, + supportsListDelete: true, + supportsTaskReparenting: true, +); + +const microsoftTaskCollectionCapabilities = TaskCollectionCapabilities( + supportsDueDate: true, + supportsDueTime: true, + supportsStartDateTime: true, + supportsReminderDateTime: true, + supportsRecurrence: true, + supportsImportance: true, + supportsCategories: true, + supportsTaskHierarchy: true, + supportsCrossListMove: false, + supportsClearCompleted: false, + supportsHiddenTasks: false, + supportsAssignedTasks: false, + supportsListRename: true, + supportsListDelete: true, +); + +const nextcloudTaskCollectionCapabilities = TaskCollectionCapabilities( + supportsDueDate: true, + supportsDueTime: true, + supportsStartDateTime: true, + supportsReminderDateTime: true, + supportsRecurrence: true, + supportsImportance: true, + supportsCategories: true, + supportsTaskHierarchy: true, + supportsCrossListMove: true, + supportsClearCompleted: true, + supportsHiddenTasks: false, + supportsAssignedTasks: false, + supportsListRename: true, + supportsListDelete: true, + supportsIcalPriority: true, + supportsPercentComplete: true, + supportsTaskStatus: true, + supportsCompletedDateTime: true, + supportsLocation: true, + supportsUrl: true, + supportsClassification: true, + supportsMultipleReminders: true, + supportsAdvancedRecurrence: true, + supportsPinning: true, + supportsSubtaskVisibility: true, + supportsDuplicate: true, + supportsNativeExport: true, + supportsTaskReparenting: true, +); + +const noTaskCollectionCapabilities = TaskCollectionCapabilities( + supportsDueDate: false, + supportsDueTime: false, + supportsStartDateTime: false, + supportsReminderDateTime: false, + supportsRecurrence: false, + supportsImportance: false, + supportsCategories: false, + supportsTaskHierarchy: false, + supportsCrossListMove: false, + supportsClearCompleted: false, + supportsHiddenTasks: false, + supportsAssignedTasks: false, + supportsListRename: false, + supportsListDelete: false, + canCreateTasks: false, + canUpdateTasks: false, + canDeleteTasks: false, +); + +/// Adapter defaults used until list-specific capabilities are available. +TaskCollectionCapabilities adapterDefaultTaskCapabilities( + BusyProvider provider, +) => switch (provider) { + BusyProvider.google => googleTaskCollectionCapabilities, + BusyProvider.microsoft => microsoftTaskCollectionCapabilities, + BusyProvider.appleICloud => noTaskCollectionCapabilities, + BusyProvider.nextcloud => nextcloudTaskCollectionCapabilities, +}; diff --git a/lib/src/features/tasks/domain/task_checklist_item.dart b/lib/src/features/tasks/domain/task_checklist_item.dart new file mode 100644 index 0000000..2031024 --- /dev/null +++ b/lib/src/features/tasks/domain/task_checklist_item.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class TaskChecklistItemEntity { + const TaskChecklistItemEntity({ + required this.id, + required this.title, + required this.completed, + required this.rawJson, + this.createdAtUtc, + this.completedAtUtc, + }); + + factory TaskChecklistItemEntity.fromJson(Map json) { + return TaskChecklistItemEntity( + id: json['id']?.toString() ?? '', + title: json['displayName']?.toString() ?? '', + completed: json['isChecked'] == true, + createdAtUtc: _dateTimeOrNull(json['createdDateTime']), + completedAtUtc: _dateTimeOrNull(json['checkedDateTime']), + rawJson: Map.unmodifiable(json), + ); + } + + final String id; + final String title; + final bool completed; + final DateTime? createdAtUtc; + final DateTime? completedAtUtc; + final Map rawJson; + + Map toJson() => Map.from(rawJson); +} + +List decodeTaskChecklistItems(String? source) { + if (source == null || source.isEmpty) return const []; + try { + final decoded = jsonDecode(source); + if (decoded is! List) return const []; + return [ + for (final value in decoded) + if (value is Map) + TaskChecklistItemEntity.fromJson(value.cast()), + ]; + } on FormatException { + return const []; + } +} + +String encodeTaskChecklistItems( + Iterable checklistItems, +) => jsonEncode([for (final item in checklistItems) item.toJson()]); + +DateTime? _dateTimeOrNull(Object? value) { + if (value == null) return null; + return DateTime.tryParse(value.toString())?.toUtc(); +} diff --git a/lib/src/features/tasks/domain/task_remote_client.dart b/lib/src/features/tasks/domain/task_remote_client.dart new file mode 100644 index 0000000..5453ef0 --- /dev/null +++ b/lib/src/features/tasks/domain/task_remote_client.dart @@ -0,0 +1,91 @@ +import 'task_remote_models.dart'; + +/// Remote task boundary shared by Google Tasks and Microsoft To Do. +/// DAV tasks use the resource-oriented DAV synchronization engine. +abstract interface class TaskRemoteClient { + Future deleteTaskList(String taskListId); + Future getTaskList(String taskListId); + Future createTaskList({required String title}); + Future listTaskListsPage({ + int maxResults = 1000, + String? pageToken, + }); + Future patchTaskList(String taskListId, TaskListPatch patch); + Future updateTaskList( + String taskListId, + TaskListPut replacement, + ); + + Future clearCompletedTasks(String taskListId); + Future deleteTask({required String taskListId, required String taskId}); + Future getTask({required String taskListId, required String taskId}); + Future createTask({ + required String taskListId, + String? parentTaskId, + String? previousSiblingTaskId, + required TaskCreate create, + }); + Future listTasksPage({ + required String taskListId, + DateTime? completedMax, + DateTime? completedMin, + DateTime? dueMax, + DateTime? dueMin, + int maxResults = 100, + String? pageToken, + bool showCompleted = true, + bool showDeleted = false, + bool showHidden = false, + DateTime? updatedMin, + bool showAssigned = false, + }); + Future moveTask({ + required String sourceTaskListId, + required String taskId, + String? parentTaskId, + String? previousSiblingTaskId, + String? destinationTaskListId, + }); + Future patchTask({ + required String taskListId, + required String taskId, + required TaskPatch patch, + }); + Future updateTask({ + required String taskListId, + required String taskId, + required TaskPut replacement, + }); +} + +/// Child-item boundary used by providers whose subtasks are not task +/// resources. Microsoft Graph models Microsoft To Do steps as checklistItem +/// children of a todoTask. +abstract interface class TaskChecklistRemoteClient { + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? pageToken, + }); + + Future createChecklistItem({ + required String taskListId, + required String taskId, + required String title, + bool completed = false, + }); + + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + String? title, + bool? completed, + }); + + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }); +} diff --git a/lib/src/features/tasks/domain/task_remote_error.dart b/lib/src/features/tasks/domain/task_remote_error.dart new file mode 100644 index 0000000..66d21d7 --- /dev/null +++ b/lib/src/features/tasks/domain/task_remote_error.dart @@ -0,0 +1,46 @@ +class TaskRemoteError implements Exception { + const TaskRemoteError({ + required this.statusCode, + this.code, + required this.message, + this.retryable = false, + this.providerDetails, + }); + + final int statusCode; + final String? code; + final String message; + final bool retryable; + final Map? providerDetails; + + @override + String toString() => 'TaskRemoteError($code, HTTP $statusCode)'; +} + +enum TaskDueKind { date, floatingDateTime, utcDateTime, zonedDateTime } + +class TaskDueValue { + const TaskDueValue({ + required this.kind, + required this.value, + this.timeZoneId, + }); + + final TaskDueKind kind; + final String value; + final String? timeZoneId; +} + +enum TaskCompletionState { needsAction, inProcess, completed, cancelled } + +class TaskReminderValue { + const TaskReminderValue({ + this.absolute, + this.relativeOffset, + this.extensionData = const {}, + }); + + final TaskDueValue? absolute; + final Duration? relativeOffset; + final Map extensionData; +} diff --git a/lib/src/google_tasks/api/google_tasks_api_models.dart b/lib/src/features/tasks/domain/task_remote_models.dart similarity index 87% rename from lib/src/google_tasks/api/google_tasks_api_models.dart rename to lib/src/features/tasks/domain/task_remote_models.dart index 09b8126..29d234b 100644 --- a/lib/src/google_tasks/api/google_tasks_api_models.dart +++ b/lib/src/features/tasks/domain/task_remote_models.dart @@ -1,4 +1,4 @@ -import 'google_tasks_json.dart'; +import '../../../google_tasks/api/google_tasks_json.dart'; class TaskListDto { const TaskListDto({ @@ -130,6 +130,36 @@ class TaskDto { final Map rawJson; } +class TaskChecklistItemDto { + const TaskChecklistItemDto({ + required this.id, + required this.title, + required this.completed, + required this.rawJson, + this.createdAtUtc, + this.completedAtUtc, + }); + + final String id; + final String title; + final bool completed; + final DateTime? createdAtUtc; + final DateTime? completedAtUtc; + final Map rawJson; +} + +class TaskChecklistItemsPageDto { + const TaskChecklistItemsPageDto({ + required this.items, + required this.rawJson, + this.nextPageToken, + }); + + final List items; + final Map rawJson; + final String? nextPageToken; +} + class TaskLinkDto { const TaskLinkDto({ required this.rawJson, @@ -179,27 +209,27 @@ class TasksPageDto { final Map rawJson; } -abstract class GoogleTasksMutation { - const GoogleTasksMutation(this.fields); +abstract class TaskMutation { + const TaskMutation(this.fields); final Map fields; Map toJson() => Map.unmodifiable(fields); } -class TaskListPatch extends GoogleTasksMutation { +class TaskListPatch extends TaskMutation { const TaskListPatch(super.fields); factory TaskListPatch.title(String title) => TaskListPatch({'title': title}); } -class TaskListPut extends GoogleTasksMutation { +class TaskListPut extends TaskMutation { const TaskListPut(super.fields); factory TaskListPut.title(String title) => TaskListPut({'title': title}); } -class TaskCreate extends GoogleTasksMutation { +class TaskCreate extends TaskMutation { const TaskCreate.fields(super.fields); factory TaskCreate({ @@ -217,7 +247,7 @@ class TaskCreate extends GoogleTasksMutation { } } -class TaskPatch extends GoogleTasksMutation { +class TaskPatch extends TaskMutation { const TaskPatch.fields(super.fields); factory TaskPatch({ @@ -243,7 +273,7 @@ class TaskPatch extends GoogleTasksMutation { } } -class TaskPut extends GoogleTasksMutation { +class TaskPut extends TaskMutation { const TaskPut.fields(super.fields); factory TaskPut({ diff --git a/lib/src/features/tasks/presentation/ical_task_fields_editor.dart b/lib/src/features/tasks/presentation/ical_task_fields_editor.dart new file mode 100644 index 0000000..ad3afd3 --- /dev/null +++ b/lib/src/features/tasks/presentation/ical_task_fields_editor.dart @@ -0,0 +1,1573 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:yaru/yaru.dart'; +import 'package:busymax/l10n/generated/app_localizations.dart'; + +import '../../../app/busymax_design.dart'; +import '../../../app/busymax_dialogs.dart'; +import '../../../dav/ical/ical_task_alarm.dart'; +import '../../../dav/ical/ical_task_recurrence.dart'; +import '../../../l10n/l10n.dart'; +import '../../../platform/linux_header_bar_service.dart'; +import '../domain/task_capabilities.dart'; +import 'desktop_date_time_fields.dart'; +import 'task_details_draft.dart'; + +/// Native RFC 5545 VTODO fields exposed by Nextcloud Tasks. +class IcalTaskFieldsEditor extends StatefulWidget { + const IcalTaskFieldsEditor({ + super.key, + required this.draft, + required this.capabilities, + required this.enabled, + required this.onChanged, + this.useNativeDatePicker = false, + this.dialogBarrierColor, + this.headerBarService, + }); + + final TaskDetailsDraft draft; + final TaskCollectionCapabilities capabilities; + final bool enabled; + final ValueChanged onChanged; + final bool useNativeDatePicker; + final Color? dialogBarrierColor; + final LinuxHeaderBarService? headerBarService; + + @override + State createState() => _IcalTaskFieldsEditorState(); +} + +class _IcalTaskFieldsEditorState extends State { + late final TextEditingController _locationController; + late final TextEditingController _urlController; + + @override + void initState() { + super.initState(); + _locationController = TextEditingController(text: widget.draft.location); + _urlController = TextEditingController(text: widget.draft.taskUrl); + } + + @override + void didUpdateWidget(covariant IcalTaskFieldsEditor oldWidget) { + super.didUpdateWidget(oldWidget); + if (_locationController.text != widget.draft.location) { + _locationController.value = TextEditingValue( + text: widget.draft.location, + selection: TextSelection.collapsed( + offset: widget.draft.location.length, + ), + ); + } + if (_urlController.text != widget.draft.taskUrl) { + _urlController.value = TextEditingValue( + text: widget.draft.taskUrl, + selection: TextSelection.collapsed(offset: widget.draft.taskUrl.length), + ); + } + } + + @override + void dispose() { + _locationController.dispose(); + _urlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final capabilities = widget.capabilities; + final result = []; + if (capabilities.supportsTaskStatus || + capabilities.supportsPercentComplete || + capabilities.supportsCompletedDateTime) { + result.add(_statusGroup()); + } + if (capabilities.supportsIcalPriority) { + result.add(_priorityGroup()); + } + if (capabilities.supportsLocation || capabilities.supportsUrl) { + result.add(_placeAndLinkGroup()); + } + if (capabilities.supportsClassification || + capabilities.supportsPinning || + capabilities.supportsSubtaskVisibility) { + result.add(_sharingGroup()); + } + if (capabilities.supportsMultipleReminders) { + result.add(_remindersGroup()); + } + if (capabilities.supportsAdvancedRecurrence && + (widget.draft.microsoftStartDate != null || + widget.draft.dueDate != null)) { + result.add(_recurrenceGroup()); + } + return Column(children: result); + } + + Widget _statusGroup() { + final l10n = context.l10n; + final draft = widget.draft; + final showCompletionDate = + draft.taskStatus == 'COMPLETED' || + draft.percentComplete == 100 || + draft.completedDate != null; + return BusyMaxGroupedList( + title: l10n.statusSection, + filled: true, + children: [ + if (widget.capabilities.supportsTaskStatus) + BusyMaxComboRow( + title: l10n.taskStatus, + leading: const Icon(Icons.fact_check_outlined), + values: const [ + '', + 'NEEDS-ACTION', + 'IN-PROCESS', + 'COMPLETED', + 'CANCELLED', + ], + selected: draft.taskStatus ?? '', + enabled: widget.enabled, + labelFor: (value) => switch (value) { + 'NEEDS-ACTION' => l10n.taskStatusNeedsAction, + 'IN-PROCESS' => l10n.taskStatusInProcess, + 'COMPLETED' => l10n.taskStatusCompleted, + 'CANCELLED' => l10n.taskStatusCancelled, + _ => l10n.taskStatusNone, + }, + onSelected: (value) => _setStatus(value.isEmpty ? null : value), + ), + if (widget.capabilities.supportsPercentComplete) + _TaskValueSliderRow( + key: const ValueKey('ical-task-progress'), + title: l10n.completionPercent(draft.percentComplete), + leading: const Icon(Icons.donut_large_outlined), + value: draft.percentComplete, + maximum: 100, + divisions: 100, + enabled: widget.enabled, + onChanged: _setProgress, + ), + if (widget.capabilities.supportsCompletedDateTime && + showCompletionDate) ...[ + DesktopDateValueRow( + label: l10n.completionDate, + date: draft.completedDate, + enabled: widget.enabled, + onChanged: _setCompletionDate, + onClear: () => _setCompletionDate(null), + useNativePicker: widget.useNativeDatePicker, + ), + if (draft.completedDate != null) + DesktopTimeValueRow( + label: l10n.completed, + time: draft.completedTime, + enabled: widget.enabled, + onChanged: (value) => + widget.onChanged(draft.copyWith(completedTime: value)), + useNativePicker: widget.useNativeDatePicker, + ), + ], + ], + ); + } + + Widget _priorityGroup() { + final l10n = context.l10n; + final priority = widget.draft.icalPriority; + final label = switch (priority) { + 0 => l10n.priorityNone, + >= 1 && <= 4 => l10n.priorityHighValue(priority), + 5 => l10n.priorityMediumValue(priority), + _ => l10n.priorityLowValue(priority), + }; + return BusyMaxGroupedList( + title: l10n.priority, + filled: true, + children: [ + _TaskValueSliderRow( + key: const ValueKey('ical-task-priority'), + title: label, + leading: const Icon(YaruIcons.task_important), + value: priority, + maximum: 9, + divisions: 9, + enabled: widget.enabled, + // RFC 5545 orders priority in the opposite direction: 1 is highest. + onChanged: (value) => + widget.onChanged(widget.draft.copyWith(icalPriority: value)), + ), + ], + ); + } + + Widget _placeAndLinkGroup() { + final l10n = context.l10n; + return BusyMaxGroupedList( + filled: true, + children: [ + if (widget.capabilities.supportsLocation) + YaruListTile.square( + leading: const Icon(Icons.place_outlined), + title: TextField( + key: const ValueKey('ical-task-location'), + controller: _locationController, + enabled: widget.enabled, + decoration: busyMaxGroupedTextFieldDecoration( + context, + labelText: l10n.location, + ), + onChanged: (value) => + widget.onChanged(widget.draft.copyWith(location: value)), + ), + ), + if (widget.capabilities.supportsUrl) + YaruListTile.square( + leading: const Icon(Icons.link), + title: TextField( + key: const ValueKey('ical-task-url'), + controller: _urlController, + enabled: widget.enabled, + keyboardType: TextInputType.url, + decoration: busyMaxGroupedTextFieldDecoration( + context, + labelText: l10n.taskUrl, + errorText: widget.draft.hasValidTaskUrl + ? null + : l10n.invalidTaskUrl, + ), + onChanged: (value) => + widget.onChanged(widget.draft.copyWith(taskUrl: value)), + ), + ), + ], + ); + } + + Widget _sharingGroup() { + final l10n = context.l10n; + return BusyMaxGroupedList( + filled: true, + children: [ + if (widget.capabilities.supportsClassification) + BusyMaxComboRow( + title: l10n.classification, + leading: const Icon(Icons.visibility_outlined), + values: const ['PUBLIC', 'CONFIDENTIAL', 'PRIVATE'], + selected: widget.draft.classification, + enabled: + widget.enabled && widget.capabilities.canUpdateClassification, + labelFor: (value) => switch (value) { + 'CONFIDENTIAL' => l10n.classificationConfidential, + 'PRIVATE' => l10n.classificationPrivate, + _ => l10n.classificationPublic, + }, + onSelected: (value) => + widget.onChanged(widget.draft.copyWith(classification: value)), + ), + if (widget.capabilities.supportsPinning) + BusyMaxSwitchRow( + title: l10n.pinTask, + leading: const Icon(Icons.push_pin_outlined), + value: widget.draft.pinned, + enabled: widget.enabled, + onChanged: (value) => + widget.onChanged(widget.draft.copyWith(pinned: value)), + ), + if (widget.capabilities.supportsSubtaskVisibility) + BusyMaxSwitchRow( + key: const ValueKey('ical-task-hide-subtasks'), + title: l10n.hideSubtasks, + leading: const Icon(Icons.account_tree_outlined), + value: widget.draft.hideSubtasks, + enabled: widget.enabled, + onChanged: (value) => + widget.onChanged(widget.draft.copyWith(hideSubtasks: value)), + ), + if (widget.capabilities.supportsSubtaskVisibility) + BusyMaxSwitchRow( + key: const ValueKey('ical-task-hide-completed-subtasks'), + title: l10n.hideClosedSubtasks, + leading: const Icon(Icons.rule_outlined), + value: widget.draft.hideCompletedSubtasks, + enabled: widget.enabled, + onChanged: (value) => widget.onChanged( + widget.draft.copyWith(hideCompletedSubtasks: value), + ), + ), + ], + ); + } + + Widget _remindersGroup() { + final l10n = context.l10n; + final alarms = widget.draft.alarms; + return BusyMaxGroupedList( + title: l10n.reminders, + filled: true, + children: [ + if (alarms.isEmpty) + BusyMaxActionRow( + title: l10n.noReminders, + leading: const Icon(Icons.notifications_none), + enabled: false, + ), + for (var index = 0; index < alarms.length; index += 1) + BusyMaxActionRow( + key: ValueKey('ical-task-alarm-$index'), + title: _alarmLabel(alarms[index]), + subtitle: _alarmTypeLabel(alarms[index]), + leading: const Icon(Icons.notifications_outlined), + enabled: widget.enabled, + onTap: _canEditReminder(alarms[index]) + ? () => _editReminder(index) + : null, + tooltip: _canEditReminder(alarms[index]) + ? l10n.editReminder + : l10n.unsupportedReminder, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_canEditReminder(alarms[index])) + YaruIconButton( + tooltip: l10n.editReminder, + icon: const Icon(Icons.edit_outlined), + onPressed: widget.enabled + ? () => _editReminder(index) + : null, + ), + YaruIconButton( + tooltip: l10n.removeReminder, + icon: const Icon(Icons.close), + onPressed: widget.enabled + ? () => _removeReminder(index) + : null, + ), + ], + ), + ), + BusyMaxActionRow( + title: l10n.addReminder, + leading: const Icon(Icons.add_alert_outlined), + enabled: widget.enabled, + onTap: widget.enabled ? _addReminder : null, + ), + ], + ); + } + + Widget _recurrenceGroup() { + final l10n = context.l10n; + final recurrence = IcalTaskRecurrence.fromJson( + widget.draft.recurrenceJson, + baseDate: _recurrenceBaseDate, + ); + final canEdit = widget.enabled && recurrence.isSupported; + return BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxActionRow( + key: const ValueKey('ical-task-recurrence'), + title: l10n.repeat, + subtitle: recurrence.isSupported + ? _recurrenceSummary(recurrence) + : l10n.unsupportedRecurrencePreserved, + leading: const Icon(YaruIcons.repeat), + enabled: canEdit, + onTap: canEdit ? _editRecurrence : null, + trailing: Icon( + recurrence.isSupported ? Icons.chevron_right : Icons.lock_outline, + ), + ), + ], + ); + } + + void _setStatus(String? status) { + final draft = widget.draft; + final now = DateTime.now(); + final next = switch (status) { + 'COMPLETED' => draft.copyWith( + taskStatus: status, + percentComplete: 100, + completedDate: draft.completedDate ?? _dateString(now), + completedTime: draft.completedTime ?? _timeString(now), + ), + 'IN-PROCESS' => draft.copyWith( + taskStatus: status, + percentComplete: draft.percentComplete == 100 + ? 99 + : draft.percentComplete == 0 + ? 1 + : draft.percentComplete, + completedDate: null, + completedTime: null, + ), + 'NEEDS-ACTION' || null => draft.copyWith( + taskStatus: status, + percentComplete: draft.percentComplete == 100 + ? 99 + : draft.percentComplete, + completedDate: null, + completedTime: null, + ), + 'CANCELLED' => draft.copyWith(taskStatus: status), + _ => draft, + }; + widget.onChanged(next); + } + + void _setProgress(int percent) { + final draft = widget.draft; + final now = DateTime.now(); + widget.onChanged(switch (percent) { + 100 => draft.copyWith( + percentComplete: 100, + taskStatus: 'COMPLETED', + completedDate: draft.completedDate ?? _dateString(now), + completedTime: draft.completedTime ?? _timeString(now), + ), + 0 => draft.copyWith( + percentComplete: 0, + taskStatus: 'NEEDS-ACTION', + completedDate: null, + completedTime: null, + ), + _ => draft.copyWith( + percentComplete: percent, + taskStatus: 'IN-PROCESS', + completedDate: null, + completedTime: null, + ), + }); + } + + void _setCompletionDate(String? value) { + final draft = widget.draft; + if (value == null) { + widget.onChanged( + draft.copyWith( + completedDate: null, + completedTime: null, + percentComplete: draft.percentComplete == 100 + ? 99 + : draft.percentComplete, + taskStatus: draft.percentComplete == 100 + ? 'IN-PROCESS' + : draft.taskStatus, + ), + ); + return; + } + widget.onChanged( + draft.copyWith( + completedDate: value, + completedTime: draft.completedTime ?? _timeString(DateTime.now()), + percentComplete: 100, + taskStatus: 'COMPLETED', + ), + ); + } + + Future _addReminder() async { + final alarm = await _showReminderDialog(context, initial: null); + if (alarm == null || !mounted) return; + widget.onChanged( + widget.draft.copyWith(alarms: [...widget.draft.alarms, alarm]), + ); + } + + Future _editReminder(int index) async { + final current = widget.draft.alarms[index]; + if (!_canEditReminder(current)) return; + final alarm = await _showReminderDialog(context, initial: current); + if (alarm == null || !mounted) return; + final alarms = [...widget.draft.alarms]; + alarms[index] = alarm; + widget.onChanged(widget.draft.copyWith(alarms: alarms)); + } + + void _removeReminder(int index) { + widget.onChanged( + widget.draft.copyWith( + alarms: [ + for (var i = 0; i < widget.draft.alarms.length; i += 1) + if (i != index) widget.draft.alarms[i], + ], + ), + ); + } + + bool _canEditReminder(IcalTaskAlarm alarm) => alarm.canEditTriggerFor( + allDay: + widget.draft.microsoftDueTime == null && + widget.draft.microsoftStartTime == null, + ); + + Future _showReminderDialog( + BuildContext context, { + required IcalTaskAlarm? initial, + }) { + return showBusyMaxModalEditorDialog( + context, + barrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + maxWidth: 520, + maxHeight: 680, + builder: (context) => _TaskReminderDialog( + initial: initial, + hasStart: widget.draft.microsoftStartDate != null, + hasDue: widget.draft.dueDate != null, + allDay: + widget.draft.microsoftDueTime == null && + widget.draft.microsoftStartTime == null, + useNativeDatePicker: widget.useNativeDatePicker, + ), + ); + } + + Future _editRecurrence() async { + final initial = IcalTaskRecurrence.fromJson( + widget.draft.recurrenceJson, + baseDate: _recurrenceBaseDate, + ); + if (!initial.isSupported) return; + final result = await showBusyMaxModalEditorDialog( + context, + barrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + maxWidth: 620, + maxHeight: 820, + builder: (context) => _TaskRecurrenceDialog( + initial: initial, + allDay: + widget.draft.microsoftDueTime == null && + widget.draft.microsoftStartTime == null, + baseDate: DateTime.now(), + minimumDate: + DateTime.tryParse(widget.draft.microsoftStartDate ?? '') ?? + DateTime.now(), + useNativeDatePicker: widget.useNativeDatePicker, + ), + ); + if (result == null || !mounted) return; + widget.onChanged( + widget.draft.copyWith(recurrenceJson: result.toJsonString()), + ); + } + + String _alarmLabel(IcalTaskAlarm alarm) { + final absolute = alarm.absoluteUtc; + if (absolute != null) { + final local = absolute.toLocal(); + return context.l10n.dateTimeDisplay( + MaterialLocalizations.of(context).formatMediumDate(local), + TimeOfDay.fromDateTime(local).format(context), + ); + } + final offset = alarm.relativeOffset; + if (offset != null) { + final absoluteSeconds = offset.inSeconds.abs(); + if (absoluteSeconds == 0) { + return alarm.isRelatedToDue + ? context.l10n.reminderAtTaskDue + : context.l10n.reminderAtTaskStart; + } + final amount = absoluteSeconds % (7 * Duration.secondsPerDay) == 0 + ? absoluteSeconds ~/ (7 * Duration.secondsPerDay) + : absoluteSeconds % Duration.secondsPerDay == 0 + ? absoluteSeconds ~/ Duration.secondsPerDay + : absoluteSeconds % Duration.secondsPerHour == 0 + ? absoluteSeconds ~/ Duration.secondsPerHour + : absoluteSeconds % Duration.secondsPerMinute == 0 + ? absoluteSeconds ~/ Duration.secondsPerMinute + : absoluteSeconds; + final unit = absoluteSeconds % (7 * Duration.secondsPerDay) == 0 + ? context.l10n.reminderUnitWeeks + : absoluteSeconds % Duration.secondsPerDay == 0 + ? context.l10n.reminderUnitDays + : absoluteSeconds % Duration.secondsPerHour == 0 + ? context.l10n.reminderUnitHours + : absoluteSeconds % Duration.secondsPerMinute == 0 + ? context.l10n.reminderUnitMinutes + : context.l10n.reminderUnitSeconds; + final relation = alarm.isRelatedToDue + ? (offset.isNegative + ? context.l10n.beforeTaskDue + : context.l10n.afterTaskDue) + : (offset.isNegative + ? context.l10n.beforeTaskStarts + : context.l10n.afterTaskStarts); + return '$amount $unit · $relation'; + } + return alarm.triggerRaw; + } + + String? _alarmTypeLabel(IcalTaskAlarm alarm) { + if (alarm.action.isEmpty || alarm.action == 'DISPLAY') return null; + return alarm.action; + } + + String _recurrenceSummary(IcalTaskRecurrence recurrence) { + if (!recurrence.repeats) return context.l10n.repeatNone; + final l10n = context.l10n; + var summary = recurrence.interval == 1 + ? switch (recurrence.frequency) { + IcalTaskRecurrenceFrequency.daily => l10n.repeatDaily, + IcalTaskRecurrenceFrequency.weekly => l10n.repeatWeekly, + IcalTaskRecurrenceFrequency.monthly => l10n.repeatMonthly, + IcalTaskRecurrenceFrequency.yearly => l10n.repeatYearly, + IcalTaskRecurrenceFrequency.none => l10n.repeatNone, + } + : switch (recurrence.frequency) { + IcalTaskRecurrenceFrequency.daily => l10n.repeatEveryDays( + recurrence.interval, + ), + IcalTaskRecurrenceFrequency.weekly => l10n.repeatEveryWeeks( + recurrence.interval, + ), + IcalTaskRecurrenceFrequency.monthly => l10n.repeatEveryMonths( + recurrence.interval, + ), + IcalTaskRecurrenceFrequency.yearly => l10n.repeatEveryYears( + recurrence.interval, + ), + IcalTaskRecurrenceFrequency.none => l10n.repeatNone, + }; + if (recurrence.frequency == IcalTaskRecurrenceFrequency.weekly && + recurrence.byDay.isNotEmpty) { + summary = + '$summary ${l10n.repeatOnDaysSummary(recurrence.byDay.map(_weekdayLabel).join(', '))}'; + } else if ((recurrence.frequency == IcalTaskRecurrenceFrequency.monthly || + recurrence.frequency == IcalTaskRecurrenceFrequency.yearly) && + recurrence.byMonthDay.isNotEmpty) { + summary = + '$summary ${l10n.repeatOnMonthDaysSummary(recurrence.byMonthDay.join(', '))}'; + } else if (recurrence.bySetPosition case final position?) { + summary = + '$summary ${l10n.repeatOnOrdinalSummary(_ordinalLabel(position), _ordinalDayLabel(recurrence.byDay))}'; + } + if (recurrence.frequency == IcalTaskRecurrenceFrequency.yearly && + recurrence.byMonth.isNotEmpty) { + summary = + '$summary ${l10n.repeatInMonthsSummary(recurrence.byMonth.map(_monthLabel).join(', '))}'; + } + if (recurrence.count != null) { + return '$summary · ${l10n.repeatTimesSummary(recurrence.count!)}'; + } + final until = recurrence.untilDate; + if (until == null) return summary; + final date = DateTime.tryParse(until); + final label = date == null + ? until + : MaterialLocalizations.of(context).formatMediumDate(date); + return '$summary · ${l10n.repeatUntilSummary(label)}'; + } + + DateTime? get _recurrenceBaseDate => DateTime.tryParse( + widget.draft.microsoftStartDate ?? widget.draft.dueDate ?? '', + ); + + String _weekdayLabel(String day) => + _localizedWeekday(context, day, abbreviated: false); + + String _monthLabel(int month) => _localizedMonth(context, month); + + String _ordinalLabel(int value) => _localizedOrdinal(context, value); + + String _ordinalDayLabel(List days) => + _localizedOrdinalDay(context, days); +} + +class _TaskValueSliderRow extends StatelessWidget { + const _TaskValueSliderRow({ + super.key, + required this.title, + required this.leading, + required this.value, + required this.maximum, + required this.divisions, + required this.enabled, + required this.onChanged, + }); + + final String title; + final Widget leading; + final int value; + final int maximum; + final int divisions; + final bool enabled; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return YaruListTile.square( + leading: leading, + title: Text(title), + subtitle: Slider( + value: value.toDouble(), + min: 0, + max: maximum.toDouble(), + divisions: divisions, + label: '$value', + onChanged: enabled + ? (value) => onChanged(value.round().clamp(0, maximum)) + : null, + ), + ); + } +} + +enum _ReminderMode { absolute, beforeStart, beforeDue, allDayStart, allDayDue } + +enum _ReminderUnit { seconds, minutes, hours, days, weeks } + +class _TaskReminderDialog extends StatefulWidget { + const _TaskReminderDialog({ + required this.initial, + required this.hasStart, + required this.hasDue, + required this.allDay, + required this.useNativeDatePicker, + }); + + final IcalTaskAlarm? initial; + final bool hasStart; + final bool hasDue; + final bool allDay; + final bool useNativeDatePicker; + + @override + State<_TaskReminderDialog> createState() => _TaskReminderDialogState(); +} + +class _TaskReminderDialogState extends State<_TaskReminderDialog> { + late _ReminderMode _mode; + late _ReminderUnit _unit; + late DateTime _absoluteLocal; + late TimeOfDay _allDayTime; + late final TextEditingController _amountController; + + @override + void initState() { + super.initState(); + final initial = widget.initial; + final offset = initial?.relativeOffset; + final tomorrow = DateTime.now().add(const Duration(days: 1)); + _absoluteLocal = + initial?.absoluteUtc?.toLocal() ?? + DateTime(tomorrow.year, tomorrow.month, tomorrow.day, 9); + if (offset != null) { + if (widget.allDay) { + final fields = IcalAllDayAlarmOffset.fromDuration(offset); + _mode = initial!.isRelatedToDue + ? _ReminderMode.allDayDue + : _ReminderMode.allDayStart; + _unit = fields.unit == IcalAllDayAlarmUnit.weeks + ? _ReminderUnit.weeks + : _ReminderUnit.days; + _amountController = TextEditingController(text: '${fields.amount}'); + _allDayTime = TimeOfDay(hour: fields.hour, minute: fields.minute); + } else { + _mode = initial!.isRelatedToDue + ? _ReminderMode.beforeDue + : _ReminderMode.beforeStart; + final seconds = offset.inSeconds.abs(); + if (seconds != 0 && seconds % (7 * Duration.secondsPerDay) == 0) { + _unit = _ReminderUnit.weeks; + _amountController = TextEditingController( + text: '${seconds ~/ (7 * Duration.secondsPerDay)}', + ); + } else if (seconds != 0 && seconds % Duration.secondsPerDay == 0) { + _unit = _ReminderUnit.days; + _amountController = TextEditingController( + text: '${seconds ~/ Duration.secondsPerDay}', + ); + } else if (seconds != 0 && seconds % Duration.secondsPerHour == 0) { + _unit = _ReminderUnit.hours; + _amountController = TextEditingController( + text: '${seconds ~/ Duration.secondsPerHour}', + ); + } else if (seconds == 0 || seconds % Duration.secondsPerMinute == 0) { + _unit = _ReminderUnit.minutes; + _amountController = TextEditingController( + text: '${seconds ~/ Duration.secondsPerMinute}', + ); + } else { + _unit = _ReminderUnit.seconds; + _amountController = TextEditingController(text: '$seconds'); + } + _allDayTime = const TimeOfDay(hour: 9, minute: 0); + } + } else { + _mode = _ReminderMode.absolute; + _unit = widget.allDay ? _ReminderUnit.days : _ReminderUnit.minutes; + _amountController = TextEditingController( + text: widget.allDay ? '0' : '10', + ); + _allDayTime = const TimeOfDay(hour: 9, minute: 0); + } + _amountController.addListener(_onAmountChanged); + } + + @override + void dispose() { + _amountController.removeListener(_onAmountChanged); + _amountController.dispose(); + super.dispose(); + } + + void _onAmountChanged() => setState(() {}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final modes = <_ReminderMode>[ + _ReminderMode.absolute, + if (widget.hasStart) + if (widget.allDay) + _ReminderMode.allDayStart + else + _ReminderMode.beforeStart, + if (widget.hasDue) + if (widget.allDay) _ReminderMode.allDayDue else _ReminderMode.beforeDue, + ]; + if (!modes.contains(_mode)) modes.add(_mode); + return BusyMaxModalEditorScaffold( + title: widget.initial == null ? l10n.addReminder : l10n.editReminder, + cancelLabel: l10n.cancel, + saveLabel: l10n.save, + onCancel: () => Navigator.pop(context), + onSave: _canSave ? _save : null, + contentMaxWidth: 480, + children: [ + BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxComboRow<_ReminderMode>( + title: l10n.reminder, + values: modes, + selected: _mode, + labelFor: (mode) => _modeLabel(mode, l10n), + onSelected: (value) => setState(() => _mode = value), + ), + ], + ), + if (_mode == _ReminderMode.absolute) + BusyMaxGroupedList( + filled: true, + children: [ + DesktopDateValueRow( + label: l10n.reminderDate, + date: _dateString(_absoluteLocal), + useNativePicker: widget.useNativeDatePicker, + onChanged: _setAbsoluteDate, + ), + DesktopTimeValueRow( + label: l10n.reminderTime, + time: _timeString(_absoluteLocal), + allowEmpty: false, + useNativePicker: widget.useNativeDatePicker, + onChanged: _setAbsoluteTime, + ), + ], + ) + else + BusyMaxGroupedList( + filled: true, + children: [ + YaruListTile.square( + title: TextField( + controller: _amountController, + autofocus: true, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: busyMaxGroupedTextFieldDecoration( + context, + labelText: l10n.reminderAmount, + ), + ), + ), + BusyMaxComboRow<_ReminderUnit>( + title: l10n.reminderUnit, + values: _availableUnits, + selected: _unit, + labelFor: (unit) => _unitLabel(unit, l10n), + onSelected: (value) => setState(() => _unit = value), + ), + if (_isAllDayRelative) + DesktopTimeValueRow( + label: l10n.reminderTimeOfDay, + time: _clockString(_allDayTime.hour, _allDayTime.minute), + allowEmpty: false, + useNativePicker: widget.useNativeDatePicker, + onChanged: _setAllDayTime, + ), + ], + ), + ], + ); + } + + void _setAbsoluteDate(String value) { + final date = DateTime.tryParse(value); + if (date == null) return; + setState(() { + _absoluteLocal = DateTime( + date.year, + date.month, + date.day, + _absoluteLocal.hour, + _absoluteLocal.minute, + ); + }); + } + + void _setAbsoluteTime(String? value) { + final time = _parseClock(value); + if (time == null) return; + setState(() { + _absoluteLocal = DateTime( + _absoluteLocal.year, + _absoluteLocal.month, + _absoluteLocal.day, + time.hour, + time.minute, + ); + }); + } + + void _setAllDayTime(String? value) { + final time = _parseClock(value); + if (time == null) return; + setState(() => _allDayTime = time); + } + + void _save() { + final initial = widget.initial; + if (_mode == _ReminderMode.absolute) { + Navigator.pop( + context, + initial?.withAbsoluteTrigger(_absoluteLocal.toUtc()) ?? + IcalTaskAlarm.displayAbsolute(_absoluteLocal.toUtc()), + ); + return; + } + final amount = int.tryParse(_amountController.text); + if (amount == null || amount < 0 || amount > 3600) return; + final relatedToDue = + _mode == _ReminderMode.beforeDue || _mode == _ReminderMode.allDayDue; + final offset = _isAllDayRelative + ? IcalAllDayAlarmOffset( + amount: amount, + unit: _unit == _ReminderUnit.weeks + ? IcalAllDayAlarmUnit.weeks + : IcalAllDayAlarmUnit.days, + hour: _allDayTime.hour, + minute: _allDayTime.minute, + ).toDuration() + : Duration(seconds: -_timedSeconds(amount)); + Navigator.pop( + context, + initial?.withRelativeTrigger(offset, relatedToDue: relatedToDue) ?? + IcalTaskAlarm.displayRelative(offset, relatedToDue: relatedToDue), + ); + } + + List<_ReminderUnit> get _availableUnits { + if (widget.allDay) { + return const [_ReminderUnit.days, _ReminderUnit.weeks]; + } + final result = <_ReminderUnit>[]; + if (_unit == _ReminderUnit.seconds) result.add(_ReminderUnit.seconds); + result.addAll(const [_ReminderUnit.minutes, _ReminderUnit.hours]); + result.addAll(const [_ReminderUnit.days, _ReminderUnit.weeks]); + return result; + } + + bool get _isAllDayRelative => + _mode == _ReminderMode.allDayStart || _mode == _ReminderMode.allDayDue; + + bool get _canSave { + if (_mode == _ReminderMode.absolute) return true; + final amount = int.tryParse(_amountController.text); + return amount != null && amount >= 0 && amount <= 3600; + } + + int _timedSeconds(int amount) => + amount * + switch (_unit) { + _ReminderUnit.seconds => 1, + _ReminderUnit.minutes => Duration.secondsPerMinute, + _ReminderUnit.hours => Duration.secondsPerHour, + _ReminderUnit.days => Duration.secondsPerDay, + _ReminderUnit.weeks => 7 * Duration.secondsPerDay, + }; + + String _modeLabel(_ReminderMode mode, AppLocalizations l10n) => + switch (mode) { + _ReminderMode.absolute => l10n.absoluteReminder, + _ReminderMode.beforeStart => l10n.beforeTaskStarts, + _ReminderMode.beforeDue => l10n.beforeTaskDue, + _ReminderMode.allDayStart => l10n.relativeToTaskStart, + _ReminderMode.allDayDue => l10n.relativeToTaskDue, + }; + + String _unitLabel(_ReminderUnit unit, AppLocalizations l10n) => + switch (unit) { + _ReminderUnit.seconds => l10n.reminderUnitSeconds, + _ReminderUnit.minutes => l10n.reminderUnitMinutes, + _ReminderUnit.hours => l10n.reminderUnitHours, + _ReminderUnit.days => l10n.reminderUnitDays, + _ReminderUnit.weeks => l10n.reminderUnitWeeks, + }; +} + +enum _RecurrenceEnd { never, until, count } + +class _TaskRecurrenceDialog extends StatefulWidget { + const _TaskRecurrenceDialog({ + required this.initial, + required this.allDay, + required this.baseDate, + required this.minimumDate, + required this.useNativeDatePicker, + }); + + final IcalTaskRecurrence initial; + final bool allDay; + final DateTime baseDate; + final DateTime minimumDate; + final bool useNativeDatePicker; + + @override + State<_TaskRecurrenceDialog> createState() => _TaskRecurrenceDialogState(); +} + +class _TaskRecurrenceDialogState extends State<_TaskRecurrenceDialog> { + late IcalTaskRecurrence _value; + late _RecurrenceEnd _end; + late final TextEditingController _intervalController; + late final TextEditingController _countController; + + @override + void initState() { + super.initState(); + _value = widget.initial.isSupported + ? widget.initial + : const IcalTaskRecurrence.none(); + _end = _value.count != null + ? _RecurrenceEnd.count + : _value.untilRaw != null + ? _RecurrenceEnd.until + : _RecurrenceEnd.never; + _intervalController = TextEditingController(text: '${_value.interval}'); + _countController = TextEditingController(text: '${_value.count ?? 10}'); + } + + @override + void dispose() { + _intervalController.dispose(); + _countController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final repeats = _value.repeats; + return BusyMaxModalEditorScaffold( + title: l10n.repeat, + cancelLabel: l10n.cancel, + saveLabel: l10n.save, + onCancel: () => Navigator.pop(context), + onSave: _isValid ? () => Navigator.pop(context, _value) : null, + contentMaxWidth: 560, + children: [ + BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxComboRow( + title: l10n.repeat, + values: IcalTaskRecurrenceFrequency.values, + selected: _value.frequency, + labelFor: _frequencyLabel, + onSelected: _setFrequency, + ), + if (repeats) + YaruListTile.square( + title: TextField( + controller: _intervalController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: busyMaxGroupedTextFieldDecoration( + context, + labelText: l10n.repeatEvery, + ), + onChanged: (value) { + final parsed = int.tryParse(value); + setState(() { + if (parsed != null && parsed >= 1 && parsed <= 366) { + _value = _value.copyWith(interval: parsed); + } + }); + }, + ), + ), + ], + ), + if (_value.frequency == IcalTaskRecurrenceFrequency.weekly) + BusyMaxGroupedList( + title: l10n.repeatOn, + filled: true, + children: [ + Padding( + padding: const EdgeInsets.all(BusyMaxSpacing.md), + child: YaruChoiceChipBar( + style: YaruChoiceChipBarStyle.wrap, + labels: [ + for (final day in _weekdays) Text(_weekdayLabel(day)), + ], + isSelected: [ + for (final day in _weekdays) _value.byDay.contains(day), + ], + selectedFirst: false, + clearOnSelect: false, + onSelected: (index) { + final day = _weekdays[index]; + _toggleWeekday(day, !_value.byDay.contains(day)); + }, + ), + ), + ], + ), + if (_value.frequency == IcalTaskRecurrenceFrequency.monthly || + _value.frequency == IcalTaskRecurrenceFrequency.yearly) + BusyMaxGroupedList( + title: l10n.repeatDayOfMonth, + filled: true, + children: [ + Padding( + padding: const EdgeInsets.all(BusyMaxSpacing.md), + child: YaruChoiceChipBar( + style: YaruChoiceChipBarStyle.wrap, + labels: [for (var day = 1; day <= 31; day += 1) Text('$day')], + isSelected: [ + for (var day = 1; day <= 31; day += 1) + _value.byMonthDay.contains(day), + ], + selectedFirst: false, + clearOnSelect: false, + onSelected: _value.bySetPosition == null + ? (index) { + final day = index + 1; + _toggleMonthDay( + day, + !_value.byMonthDay.contains(day), + ); + } + : null, + ), + ), + BusyMaxComboRow( + title: l10n.repeatOrdinal, + values: const [0, ..._ordinalPositions], + selected: _value.bySetPosition ?? 0, + labelFor: (value) => value == 0 + ? l10n.repeatSpecificDays + : _localizedOrdinal(context, value), + onSelected: _setOrdinal, + ), + if (_value.bySetPosition != null) + BusyMaxComboRow( + title: l10n.repeatOn, + values: [for (final choice in _ordinalDayChoices) choice.key], + selected: _ordinalDayChoiceKey(_value.byDay), + labelFor: (value) => _localizedOrdinalDay( + context, + _ordinalDayChoices + .firstWhere((choice) => choice.key == value) + .days, + ), + onSelected: (value) { + final days = _ordinalDayChoices + .firstWhere((choice) => choice.key == value) + .days; + setState(() => _value = _value.copyWith(byDay: days)); + }, + ), + ], + ), + if (_value.frequency == IcalTaskRecurrenceFrequency.yearly) + BusyMaxGroupedList( + title: l10n.repeatMonths, + filled: true, + children: [ + Padding( + padding: const EdgeInsets.all(BusyMaxSpacing.md), + child: YaruChoiceChipBar( + style: YaruChoiceChipBarStyle.wrap, + labels: [ + for (var month = 1; month <= 12; month += 1) + Text(_localizedMonth(context, month)), + ], + isSelected: [ + for (var month = 1; month <= 12; month += 1) + _value.byMonth.contains(month), + ], + selectedFirst: false, + clearOnSelect: false, + onSelected: (index) { + final month = index + 1; + _toggleMonth(month, !_value.byMonth.contains(month)); + }, + ), + ), + ], + ), + if (repeats) + BusyMaxGroupedList( + filled: true, + children: [ + BusyMaxComboRow<_RecurrenceEnd>( + title: l10n.repeatEnd, + values: _RecurrenceEnd.values, + selected: _end, + labelFor: (value) => switch (value) { + _RecurrenceEnd.never => l10n.repeatNever, + _RecurrenceEnd.until => l10n.repeatUntil, + _RecurrenceEnd.count => l10n.repeatAfter, + }, + onSelected: _setEnd, + ), + if (_end == _RecurrenceEnd.until) + DesktopDateValueRow( + label: l10n.repeatUntil, + date: + _value.untilDate ?? + _dateString(_oneMonthAfter(DateTime.now())), + useNativePicker: widget.useNativeDatePicker, + onChanged: _setUntilDate, + ), + if (_end == _RecurrenceEnd.count) + YaruListTile.square( + title: TextField( + controller: _countController, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: busyMaxGroupedTextFieldDecoration( + context, + labelText: l10n.repeatCount, + ), + onChanged: (value) { + final parsed = int.tryParse(value); + setState(() { + if (parsed != null && parsed >= 1 && parsed <= 3500) { + _value = _value.copyWith(count: parsed); + } + }); + }, + ), + ), + ], + ), + ], + ); + } + + bool get _isValid { + if (!_value.repeats) return true; + final interval = int.tryParse(_intervalController.text); + if (interval == null || interval < 1 || interval > 366) return false; + if (_value.frequency == IcalTaskRecurrenceFrequency.weekly && + _value.byDay.isEmpty) { + return false; + } + if ((_value.frequency == IcalTaskRecurrenceFrequency.monthly || + _value.frequency == IcalTaskRecurrenceFrequency.yearly) && + _value.byMonthDay.isEmpty && + (_value.bySetPosition == null || _value.byDay.isEmpty)) { + return false; + } + if (_value.frequency == IcalTaskRecurrenceFrequency.yearly && + _value.byMonth.isEmpty) { + return false; + } + if (_end == _RecurrenceEnd.until && _value.untilRaw == null) return false; + if (_end == _RecurrenceEnd.count) { + final count = int.tryParse(_countController.text); + if (count == null || count < 1 || count > 3500) return false; + } + return true; + } + + void _setFrequency(IcalTaskRecurrenceFrequency frequency) { + final now = DateTime.now(); + setState(() { + _value = switch (frequency) { + IcalTaskRecurrenceFrequency.none => _value.copyWith( + frequency: frequency, + byDay: const [], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + count: null, + untilRaw: null, + ), + IcalTaskRecurrenceFrequency.weekly => _value.copyWith( + frequency: frequency, + byDay: [_weekdayCode(now.weekday)], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + ), + IcalTaskRecurrenceFrequency.monthly => _value.copyWith( + frequency: frequency, + byDay: const [], + byMonth: const [], + byMonthDay: [now.day], + bySetPosition: null, + ), + IcalTaskRecurrenceFrequency.yearly => _value.copyWith( + frequency: frequency, + byDay: const [], + byMonth: [now.month], + byMonthDay: [now.day], + bySetPosition: null, + ), + IcalTaskRecurrenceFrequency.daily => _value.copyWith( + frequency: frequency, + byDay: const [], + byMonth: const [], + byMonthDay: const [], + bySetPosition: null, + ), + }; + }); + } + + void _toggleWeekday(String day, bool selected) { + final values = [..._value.byDay]; + if (selected) { + if (!values.contains(day)) values.add(day); + } else if (values.length > 1) { + values.remove(day); + } + values.sort( + (left, right) => + _weekdays.indexOf(left).compareTo(_weekdays.indexOf(right)), + ); + setState(() => _value = _value.copyWith(byDay: values)); + } + + void _toggleMonthDay(int day, bool selected) { + final values = [..._value.byMonthDay]; + if (selected) { + if (!values.contains(day)) values.add(day); + } else if (values.length > 1) { + values.remove(day); + } + values.sort(); + setState(() { + _value = _value.copyWith( + byMonthDay: values, + byDay: const [], + bySetPosition: null, + ); + }); + } + + void _toggleMonth(int month, bool selected) { + final values = [..._value.byMonth]; + if (selected) { + if (!values.contains(month)) values.add(month); + } else if (values.length > 1) { + values.remove(month); + } + values.sort(); + setState(() => _value = _value.copyWith(byMonth: values)); + } + + void _setOrdinal(int? value) { + setState(() { + if (value == null || value == 0) { + _value = _value.copyWith( + bySetPosition: null, + byDay: const [], + byMonthDay: _value.byMonthDay.isEmpty + ? [widget.baseDate.day] + : _value.byMonthDay, + ); + } else { + _value = _value.copyWith( + bySetPosition: value, + byDay: _value.byDay.isEmpty + ? [_weekdayCode(widget.baseDate.weekday)] + : _value.byDay, + byMonthDay: const [], + ); + } + }); + } + + void _setEnd(_RecurrenceEnd value) { + setState(() { + _end = value; + _value = switch (value) { + _RecurrenceEnd.never => _value.copyWith(count: null, untilRaw: null), + _RecurrenceEnd.count => _value.copyWith( + count: int.tryParse(_countController.text) ?? 10, + untilRaw: null, + ), + _RecurrenceEnd.until => + _value + .copyWith(count: null) + .withUntilDate( + _value.untilDate ?? _dateString(_oneMonthAfter(DateTime.now())), + allDay: widget.allDay, + ), + }; + }); + } + + void _setUntilDate(String value) { + final date = DateTime.tryParse(value); + if (date == null) return; + final firstDate = DateTime( + widget.minimumDate.year, + widget.minimumDate.month, + widget.minimumDate.day, + ); + if (date.isBefore(firstDate)) return; + setState(() { + _value = _value.withUntilDate(_dateString(date), allDay: widget.allDay); + }); + } + + String _frequencyLabel(IcalTaskRecurrenceFrequency frequency) => + switch (frequency) { + IcalTaskRecurrenceFrequency.none => context.l10n.repeatNone, + IcalTaskRecurrenceFrequency.daily => context.l10n.repeatDaily, + IcalTaskRecurrenceFrequency.weekly => context.l10n.repeatWeekly, + IcalTaskRecurrenceFrequency.monthly => context.l10n.repeatMonthly, + IcalTaskRecurrenceFrequency.yearly => context.l10n.repeatYearly, + }; + + String _weekdayLabel(String day) { + return _localizedWeekday(context, day, abbreviated: true); + } +} + +const _weekdays = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']; +const _ordinalPositions = [1, 2, 3, 4, 5, -2, -1]; + +typedef _OrdinalDayChoice = ({String key, List days}); + +const _ordinalDayChoices = <_OrdinalDayChoice>[ + (key: 'MO', days: ['MO']), + (key: 'TU', days: ['TU']), + (key: 'WE', days: ['WE']), + (key: 'TH', days: ['TH']), + (key: 'FR', days: ['FR']), + (key: 'SA', days: ['SA']), + (key: 'SU', days: ['SU']), + (key: 'day', days: ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU']), + (key: 'weekday', days: ['MO', 'TU', 'WE', 'TH', 'FR']), + (key: 'weekend', days: ['SA', 'SU']), +]; + +String _weekdayCode(int weekday) => _weekdays[(weekday - 1).clamp(0, 6)]; + +String _ordinalDayChoiceKey(List days) { + for (final choice in _ordinalDayChoices) { + if (_sameDaySet(choice.days, days)) return choice.key; + } + return days.firstOrNull ?? 'MO'; +} + +String _localizedWeekday( + BuildContext context, + String day, { + required bool abbreviated, +}) { + final index = _weekdays.indexOf(day); + if (index < 0) return day; + final date = DateTime(2024, 1, 1 + index); + final locale = Localizations.localeOf(context).toLanguageTag(); + return DateFormat(abbreviated ? 'EEE' : 'EEEE', locale).format(date); +} + +String _localizedMonth(BuildContext context, int month) { + if (month < 1 || month > 12) return '$month'; + return DateFormat( + 'MMM', + Localizations.localeOf(context).toLanguageTag(), + ).format(DateTime(2024, month)); +} + +String _localizedOrdinal(BuildContext context, int value) => switch (value) { + 1 => context.l10n.repeatFirst, + 2 => context.l10n.repeatSecond, + 3 => context.l10n.repeatThird, + 4 => context.l10n.repeatFourth, + 5 => context.l10n.repeatFifth, + -2 => context.l10n.repeatSecondToLast, + -1 => context.l10n.repeatLast, + _ => '$value', +}; + +String _localizedOrdinalDay(BuildContext context, List days) { + if (days.length == 1) { + return _localizedWeekday(context, days.single, abbreviated: false); + } + if (_sameDaySet(days, _ordinalDayChoices[7].days)) { + return context.l10n.repeatAnyDay; + } + if (_sameDaySet(days, _ordinalDayChoices[8].days)) { + return context.l10n.repeatWeekday; + } + if (_sameDaySet(days, _ordinalDayChoices[9].days)) { + return context.l10n.repeatWeekendDay; + } + return days + .map((day) => _localizedWeekday(context, day, abbreviated: false)) + .join(', '); +} + +bool _sameDaySet(List left, List right) { + if (left.length != right.length) return false; + final leftValues = {...left}; + return leftValues.length == left.length && leftValues.containsAll(right); +} + +DateTime _oneMonthAfter(DateTime value) { + final firstOfTarget = DateTime(value.year, value.month + 1); + final lastDay = DateTime(firstOfTarget.year, firstOfTarget.month + 1, 0).day; + final day = value.day > lastDay ? lastDay : value.day; + return DateTime(firstOfTarget.year, firstOfTarget.month, day); +} + +String _dateString(DateTime value) => + '${value.year.toString().padLeft(4, '0')}-' + '${value.month.toString().padLeft(2, '0')}-' + '${value.day.toString().padLeft(2, '0')}'; + +String _timeString(DateTime value) => _clockString(value.hour, value.minute); + +String _clockString(int hour, int minute) => + '${hour.toString().padLeft(2, '0')}:' + '${minute.toString().padLeft(2, '0')}'; + +TimeOfDay? _parseClock(String? value) { + final match = RegExp(r'^(\d{2}):(\d{2})$').firstMatch(value ?? ''); + if (match == null) return null; + final hour = int.parse(match.group(1)!); + final minute = int.parse(match.group(2)!); + if (hour > 23 || minute > 59) return null; + return TimeOfDay(hour: hour, minute: minute); +} diff --git a/lib/src/features/tasks/presentation/new_task_dialog.dart b/lib/src/features/tasks/presentation/new_task_dialog.dart index 4d3fba6..5333d6f 100644 --- a/lib/src/features/tasks/presentation/new_task_dialog.dart +++ b/lib/src/features/tasks/presentation/new_task_dialog.dart @@ -8,7 +8,8 @@ import '../../../app/busymax_dialogs.dart'; import '../../../google_tasks/api/google_tasks_json.dart'; import '../../../l10n/l10n.dart'; import '../../../platform/linux_header_bar_service.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import '../../accounts/data/accounts_repository.dart'; import '../../task_lists/data/task_lists_repository.dart'; import '../data/tasks_repository.dart'; @@ -73,7 +74,7 @@ class NewTaskEditorPanel extends ConsumerStatefulWidget { this.initialDueUtc, this.categorySuggestionsForAccount, this.useNativeDatePicker = false, - }); + }) : assert(accounts.length > 0, 'A task editor requires an account.'); final List accounts; final String? initialAccountId; @@ -106,9 +107,8 @@ class _NewTaskEditorPanelState extends ConsumerState { @override Widget build(BuildContext context) { final accountId = _accountId ?? widget.accounts.first.id; - final account = _accountForId(accountId); - final provider = account?.provider ?? TaskProvider.google; - final capabilities = capabilitiesForProvider(provider); + final account = _requireAccount(accountId); + final provider = account.provider; final localTimeZone = ref.watch(localTimeZoneProvider); final repository = ref.watch( taskListsRepositoryForAccountProvider(accountId), @@ -128,6 +128,11 @@ class _NewTaskEditorPanelState extends ConsumerState { builder: (context, snapshot) { final taskLists = snapshot.data ?? const []; final effectiveListId = _effectiveListId(taskLists); + final capabilities = _capabilitiesFor( + provider, + accountId, + effectiveListId, + ); final editorTask = _newTaskEntity( accountId: accountId, taskListId: effectiveListId ?? '', @@ -152,23 +157,27 @@ class _NewTaskEditorPanelState extends ConsumerState { accountIds: [for (final account in widget.accounts) account.id], selectedAccountId: accountId, accountLabelFor: _accountLabel, - accountSecondaryLabelFor: _accountSecondaryLabel, onAccountSelected: _selectAccount, allowTaskListSelection: true, showAdvancedActions: false, showDeleteAction: false, + isCreate: true, confirmTaskSwitch: false, useNativeDatePicker: widget.useNativeDatePicker, headerBarService: ref.read(linuxHeaderBarServiceProvider), categorySuggestions: categorySuggestions, canSaveDraft: (draft) => draft.taskListId.isNotEmpty, onDraftChanged: (draft) { - _draftSnapshot = draft; + if (_draftSnapshot?.taskListId != draft.taskListId) { + setState(() => _draftSnapshot = draft); + } else { + _draftSnapshot = draft; + } }, onRefresh: () {}, onSave: (draft, _) => _submit(draft, capabilities, localTimeZone: localTimeZone), - onCreateSubtask: (_) {}, + onCreateSubtask: (_) async {}, onMoveToTop: () {}, onDelete: () async {}, onCancel: widget.onCancel, @@ -193,6 +202,26 @@ class _NewTaskEditorPanelState extends ConsumerState { return taskLists.first.id; } + TaskCollectionCapabilities _capabilitiesFor( + BusyProvider provider, + String accountId, + String? taskListId, + ) { + if (provider == BusyProvider.google || provider == BusyProvider.microsoft) { + return adapterDefaultTaskCapabilities(provider); + } + if (taskListId == null) return noTaskCollectionCapabilities; + return ref + .watch( + davTaskCollectionCapabilitiesProvider(( + accountId: accountId, + taskListId: taskListId, + )), + ) + .valueOrNull ?? + noTaskCollectionCapabilities; + } + TaskDetailsDraft? _initialDraftFor( TaskEntity editorTask, String? effectiveListId, @@ -240,18 +269,22 @@ class _NewTaskEditorPanelState extends ConsumerState { return null; } - String _accountLabel(String accountId) { - return _accountForId(accountId)?.displayLabel ?? accountId; + AccountEntity _requireAccount(String accountId) { + final account = _accountForId(accountId); + if (account == null) { + throw StateError('The selected task account is unavailable.'); + } + return account; } - String? _accountSecondaryLabel(String accountId) { - return _accountForId(accountId)?.secondaryLabel; + String _accountLabel(String accountId) { + return _accountForId(accountId)?.selectorLabel ?? accountId; } String _accountEditorLabel( BuildContext context, AccountEntity? account, - TaskProvider provider, + BusyProvider provider, ) { final label = account?.displayLabel.trim(); if (label != null && label.isNotEmpty) { @@ -264,8 +297,8 @@ class _NewTaskEditorPanelState extends ConsumerState { if (value == _accountId) { return; } - final provider = _accountForId(value)?.provider ?? TaskProvider.google; - final capabilities = capabilitiesForProvider(provider); + final provider = _requireAccount(value).provider; + final capabilities = adapterDefaultTaskCapabilities(provider); setState(() { _accountId = value; _taskListId = null; @@ -277,7 +310,7 @@ class _NewTaskEditorPanelState extends ConsumerState { Future _submit( TaskDetailsDraft draft, - TaskProviderCapabilities capabilities, { + TaskCollectionCapabilities capabilities, { required String localTimeZone, }) async { final accountId = _accountId; diff --git a/lib/src/features/tasks/presentation/task_details_draft.dart b/lib/src/features/tasks/presentation/task_details_draft.dart index bd50414..fe10242 100644 --- a/lib/src/features/tasks/presentation/task_details_draft.dart +++ b/lib/src/features/tasks/presentation/task_details_draft.dart @@ -1,10 +1,16 @@ import 'dart:convert'; +import 'package:timezone/data/latest_all.dart' as time_zone_data; +import 'package:timezone/timezone.dart' as time_zone; + import '../../../core/time/provider_date_time.dart'; +import '../../../dav/ical/ical_task_alarm.dart'; import '../../../google_tasks/api/google_tasks_json.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import '../data/tasks_repository.dart'; +enum TaskScheduleIssue { none, dueBeforeStart, mixedTimeModes } + class TaskDetailsDraft { const TaskDetailsDraft({ required this.taskListId, @@ -24,6 +30,18 @@ class TaskDetailsDraft { required this.recurrenceJson, required this.importance, required this.categories, + required this.icalPriority, + required this.percentComplete, + required this.taskStatus, + required this.completedDate, + required this.completedTime, + required this.location, + required this.taskUrl, + required this.classification, + required this.pinned, + required this.hideSubtasks, + required this.hideCompletedSubtasks, + required this.alarms, }); factory TaskDetailsDraft.fromTask(TaskEntity task, String localTimeZone) { @@ -72,6 +90,18 @@ class TaskDetailsDraft { recurrenceJson: task.recurrenceJson, importance: _importanceValue(task.importance), categories: _categories(task.categoriesJson), + icalPriority: (task.icalPriority ?? 0).clamp(0, 9), + percentComplete: (task.percentComplete ?? 0).clamp(0, 100), + taskStatus: _icalStatus(task.providerStatus), + completedDate: _localDatePart(task.completedUtc), + completedTime: _localTimePart(task.completedUtc), + location: task.taskLocation ?? '', + taskUrl: task.taskUrl ?? '', + classification: _classificationValue(task.taskClassification), + pinned: task.taskPinned ?? false, + hideSubtasks: task.taskHideSubtasks ?? false, + hideCompletedSubtasks: task.taskHideCompletedSubtasks ?? false, + alarms: decodeIcalTaskAlarms(task.taskAlarmsJson), ); } @@ -92,6 +122,77 @@ class TaskDetailsDraft { final String? recurrenceJson; final String importance; final List categories; + final int icalPriority; + final int percentComplete; + final String? taskStatus; + final String? completedDate; + final String? completedTime; + final String location; + final String taskUrl; + final String classification; + final bool pinned; + final bool hideSubtasks; + final bool hideCompletedSubtasks; + final List alarms; + + bool get hasValidTaskUrl { + final value = taskUrl.trim(); + if (value.isEmpty) return true; + if (value.contains('\r') || value.contains('\n')) return false; + final parsed = Uri.tryParse(value); + return parsed != null && parsed.hasScheme; + } + + DateTime? reminderReferenceUtc({ + required bool due, + required String localTimeZone, + }) { + final date = DateTime.tryParse( + due ? dueDate ?? '' : microsoftStartDate ?? '', + ); + if (date == null) return null; + final time = due ? microsoftDueTime : microsoftStartTime; + final zone = due ? microsoftDueTimeZone : microsoftStartTimeZone; + return _taskScheduleInstant( + date, + time ?? '00:00', + time == null ? localTimeZone : zone ?? localTimeZone, + ); + } + + TaskScheduleIssue get scheduleIssue { + final due = DateTime.tryParse(dueDate ?? ''); + final start = DateTime.tryParse(microsoftStartDate ?? ''); + if (due == null || start == null) { + return TaskScheduleIssue.none; + } + final dueHasTime = microsoftDueTime != null; + final startHasTime = microsoftStartTime != null; + if (dueHasTime != startHasTime) { + return TaskScheduleIssue.mixedTimeModes; + } + if (!dueHasTime) { + return _dateOnlyValue(due).isBefore(_dateOnlyValue(start)) + ? TaskScheduleIssue.dueBeforeStart + : TaskScheduleIssue.none; + } + final dueInstant = _taskScheduleInstant( + due, + microsoftDueTime!, + microsoftDueTimeZone, + ); + final startInstant = _taskScheduleInstant( + start, + microsoftStartTime!, + microsoftStartTimeZone, + ); + if (dueInstant == null || startInstant == null) { + return TaskScheduleIssue.none; + } + return dueInstant.isBefore(startInstant) + ? TaskScheduleIssue.dueBeforeStart + : TaskScheduleIssue.none; + } bool hasSameValues(TaskDetailsDraft other) { return taskListId == other.taskListId && @@ -110,12 +211,24 @@ class TaskDetailsDraft { microsoftReminderTimeZone == other.microsoftReminderTimeZone && recurrenceJson == other.recurrenceJson && importance == other.importance && - _sameStrings(categories, other.categories); + _sameStrings(categories, other.categories) && + icalPriority == other.icalPriority && + percentComplete == other.percentComplete && + taskStatus == other.taskStatus && + completedDate == other.completedDate && + completedTime == other.completedTime && + location == other.location && + taskUrl == other.taskUrl && + classification == other.classification && + pinned == other.pinned && + hideSubtasks == other.hideSubtasks && + hideCompletedSubtasks == other.hideCompletedSubtasks && + _sameAlarms(alarms, other.alarms); } bool differsFrom( TaskEntity task, - TaskProviderCapabilities capabilities, { + TaskCollectionCapabilities capabilities, { required String localTimeZone, }) { if (taskListId != task.taskListId) { @@ -126,7 +239,7 @@ class TaskDetailsDraft { Map toPatch( TaskEntity original, - TaskProviderCapabilities capabilities, { + TaskCollectionCapabilities capabilities, { required String localTimeZone, }) { final fields = {}; @@ -226,6 +339,7 @@ class TaskDetailsDraft { : jsonDecode(recurrenceJson!); } if (capabilities.supportsImportance && + !capabilities.supportsIcalPriority && importance != _importanceValue(original.importance)) { fields['importance'] = importance; } @@ -233,12 +347,59 @@ class TaskDetailsDraft { !_sameStrings(categories, _categories(original.categoriesJson))) { fields['categories'] = categories; } + if (capabilities.supportsIcalPriority && + icalPriority != (original.icalPriority ?? 0).clamp(0, 9)) { + fields['icalPriority'] = icalPriority; + } + if (capabilities.supportsPercentComplete && + percentComplete != (original.percentComplete ?? 0).clamp(0, 100)) { + fields['percentComplete'] = percentComplete; + } + if (capabilities.supportsTaskStatus && + taskStatus != _icalStatus(original.providerStatus)) { + fields['taskStatus'] = taskStatus; + } + if (capabilities.supportsCompletedDateTime) { + final desiredCompleted = _completedUtc(completedDate, completedTime); + if (!_sameInstant(desiredCompleted, original.completedUtc)) { + fields['completedAtUtc'] = desiredCompleted; + } + } + if (capabilities.supportsLocation && + location != (original.taskLocation ?? '')) { + fields['location'] = location; + } + if (capabilities.supportsUrl && taskUrl != (original.taskUrl ?? '')) { + fields['taskUrl'] = taskUrl; + } + if (capabilities.supportsClassification && + capabilities.canUpdateClassification && + classification != _classificationValue(original.taskClassification)) { + fields['taskClassification'] = classification; + } + if (capabilities.supportsPinning && + pinned != (original.taskPinned ?? false)) { + fields['taskPinned'] = pinned; + } + if (capabilities.supportsSubtaskVisibility && + hideSubtasks != (original.taskHideSubtasks ?? false)) { + fields['taskHideSubtasks'] = hideSubtasks; + } + if (capabilities.supportsSubtaskVisibility && + hideCompletedSubtasks != + (original.taskHideCompletedSubtasks ?? false)) { + fields['taskHideCompletedSubtasks'] = hideCompletedSubtasks; + } + if (capabilities.supportsMultipleReminders && + !_sameAlarms(alarms, decodeIcalTaskAlarms(original.taskAlarmsJson))) { + fields['taskAlarms'] = [for (final alarm in alarms) alarm.toJson()]; + } return fields; } TaskCreateInput toCreateInput( - TaskProviderCapabilities capabilities, { + TaskCollectionCapabilities capabilities, { required String localTimeZone, }) { final baseline = TaskEntity( @@ -288,6 +449,18 @@ class TaskDetailsDraft { Object? recurrenceJson = _unchanged, String? importance, List? categories, + int? icalPriority, + int? percentComplete, + Object? taskStatus = _unchanged, + Object? completedDate = _unchanged, + Object? completedTime = _unchanged, + String? location, + String? taskUrl, + String? classification, + bool? pinned, + bool? hideSubtasks, + bool? hideCompletedSubtasks, + List? alarms, }) { return TaskDetailsDraft( taskListId: taskListId ?? this.taskListId, @@ -322,12 +495,72 @@ class TaskDetailsDraft { : recurrenceJson as String?, importance: importance ?? this.importance, categories: categories ?? this.categories, + icalPriority: icalPriority ?? this.icalPriority, + percentComplete: percentComplete ?? this.percentComplete, + taskStatus: taskStatus == _unchanged + ? this.taskStatus + : taskStatus as String?, + completedDate: completedDate == _unchanged + ? this.completedDate + : completedDate as String?, + completedTime: completedTime == _unchanged + ? this.completedTime + : completedTime as String?, + location: location ?? this.location, + taskUrl: taskUrl ?? this.taskUrl, + classification: classification ?? this.classification, + pinned: pinned ?? this.pinned, + hideSubtasks: hideSubtasks ?? this.hideSubtasks, + hideCompletedSubtasks: + hideCompletedSubtasks ?? this.hideCompletedSubtasks, + alarms: List.unmodifiable(alarms ?? this.alarms), ); } } const _unchanged = Object(); +var _taskScheduleTimeZonesInitialized = false; + +DateTime _dateOnlyValue(DateTime value) => + DateTime.utc(value.year, value.month, value.day); + +DateTime? _taskScheduleInstant(DateTime date, String time, String? timeZoneId) { + final parts = time.split(':'); + if (parts.length < 2) { + return null; + } + final hour = int.tryParse(parts[0]); + final minute = int.tryParse(parts[1]); + final second = parts.length > 2 ? int.tryParse(parts[2]) ?? 0 : 0; + if (hour == null || minute == null) { + return null; + } + if (!_taskScheduleTimeZonesInitialized) { + time_zone_data.initializeTimeZones(); + _taskScheduleTimeZonesInitialized = true; + } + final requestedZone = timeZoneId?.trim(); + final normalizedZone = requestedZone == null || requestedZone.isEmpty + ? 'Etc/UTC' + : requestedZone == 'UTC' + ? 'Etc/UTC' + : requestedZone; + try { + return time_zone.TZDateTime( + time_zone.getLocation(normalizedZone), + date.year, + date.month, + date.day, + hour, + minute, + second, + ).toUtc(); + } on time_zone.LocationNotFoundException { + return DateTime.utc(date.year, date.month, date.day, hour, minute, second); + } +} + void _putDateTimePatch( Map fields, { required String? originalDateTime, @@ -479,3 +712,55 @@ bool _sameStrings(List left, List right) { } return true; } + +bool _sameAlarms(List left, List right) { + if (left.length != right.length) return false; + for (var index = 0; index < left.length; index += 1) { + if (left[index] != right[index]) return false; + } + return true; +} + +String? _icalStatus(String? value) => switch (value?.toUpperCase()) { + 'NEEDS-ACTION' || + 'IN-PROCESS' || + 'COMPLETED' || + 'CANCELLED' => value!.toUpperCase(), + _ => null, +}; + +String _classificationValue(String? value) => switch (value?.toUpperCase()) { + 'PRIVATE' || 'CONFIDENTIAL' => value!.toUpperCase(), + _ => 'PUBLIC', +}; + +String? _localDatePart(String? value) { + final parsed = DateTime.tryParse(value ?? ''); + if (parsed == null) return null; + final local = parsed.toLocal(); + return '${local.year.toString().padLeft(4, '0')}-' + '${local.month.toString().padLeft(2, '0')}-' + '${local.day.toString().padLeft(2, '0')}'; +} + +String? _localTimePart(String? value) { + final parsed = DateTime.tryParse(value ?? ''); + if (parsed == null) return null; + final local = parsed.toLocal(); + return '${local.hour.toString().padLeft(2, '0')}:' + '${local.minute.toString().padLeft(2, '0')}'; +} + +String? _completedUtc(String? date, String? time) { + if (date == null || date.isEmpty) return null; + final parsed = DateTime.tryParse('${date}T${time ?? '00:00'}:00'); + return parsed?.toUtc().toIso8601String(); +} + +bool _sameInstant(String? left, String? right) { + if (left == null || right == null) return left == right; + final leftValue = DateTime.tryParse(left); + final rightValue = DateTime.tryParse(right); + if (leftValue == null || rightValue == null) return left == right; + return leftValue.toUtc() == rightValue.toUtc(); +} diff --git a/lib/src/features/tasks/presentation/task_details_editor.dart b/lib/src/features/tasks/presentation/task_details_editor.dart index e4f210d..62ca2fb 100644 --- a/lib/src/features/tasks/presentation/task_details_editor.dart +++ b/lib/src/features/tasks/presentation/task_details_editor.dart @@ -11,10 +11,11 @@ import '../../../app/busymax_glyphs.dart'; import '../../../google_tasks/api/google_tasks_json.dart'; import '../../../l10n/l10n.dart'; import '../../../platform/linux_header_bar_service.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import '../../task_lists/data/task_lists_repository.dart'; import '../data/tasks_repository.dart'; import 'desktop_date_time_fields.dart'; +import 'ical_task_fields_editor.dart'; import 'task_details_draft.dart'; class TaskDetailsEditor extends StatefulWidget { @@ -31,6 +32,13 @@ class TaskDetailsEditor extends StatefulWidget { required this.onMoveToTop, required this.onDelete, required this.onCancel, + this.hierarchy = const TaskHierarchySnapshot(parent: null, subtasks: []), + this.onHierarchyTaskSelected, + this.onSubtaskCompletionChanged, + this.onChecklistSubtaskRenamed, + this.onChecklistSubtaskDeleted, + this.onDuplicate, + this.onExport, this.onSaved, this.onTaskSwitchCancelled, this.onDirtyChanged, @@ -52,11 +60,12 @@ class TaskDetailsEditor extends StatefulWidget { this.dialogBarrierColor, this.headerBarService, this.canSaveDraft, + this.isCreate = false, }); final TaskEntity task; final List taskLists; - final TaskProviderCapabilities capabilities; + final TaskCollectionCapabilities capabilities; final String localTimeZone; final String? accountLabel; final VoidCallback onRefresh; @@ -65,10 +74,20 @@ class TaskDetailsEditor extends StatefulWidget { Map patch, ) onSave; - final ValueChanged onCreateSubtask; + final Future Function(String title) onCreateSubtask; final VoidCallback onMoveToTop; final Future Function() onDelete; final VoidCallback onCancel; + final TaskHierarchySnapshot hierarchy; + final Future Function(TaskEntity task)? onHierarchyTaskSelected; + final Future Function(TaskSubtaskEntity subtask, bool completed)? + onSubtaskCompletionChanged; + final Future Function(TaskSubtaskEntity subtask, String title)? + onChecklistSubtaskRenamed; + final Future Function(TaskSubtaskEntity subtask)? + onChecklistSubtaskDeleted; + final Future Function()? onDuplicate; + final Future Function()? onExport; final VoidCallback? onSaved; final ValueChanged? onTaskSwitchCancelled; final ValueChanged? onDirtyChanged; @@ -90,6 +109,7 @@ class TaskDetailsEditor extends StatefulWidget { final Color? dialogBarrierColor; final LinuxHeaderBarService? headerBarService; final bool Function(TaskDetailsDraft draft)? canSaveDraft; + final bool isCreate; @override State createState() => _TaskDetailsEditorState(); @@ -109,6 +129,7 @@ class _TaskDetailsEditorState extends State { var _addingCategory = false; var _confirmingTaskSwitch = false; var _confirmingDelete = false; + var _creatingSubtask = false; @override void initState() { @@ -153,10 +174,14 @@ class _TaskDetailsEditorState extends State { final l10n = context.l10n; final hasChanges = _hasEditorChanges(draft); final scheduledAllDay = _isScheduledAllDay(draft); + final scheduleIssue = draft.scheduleIssue; final canSave = + _canWrite && draft.title.trim().isNotEmpty && hasChanges && !_saving && + scheduleIssue == TaskScheduleIssue.none && + draft.hasValidTaskUrl && _timeFieldsAreValid(draft, scheduledAllDay) && (widget.canSaveDraft?.call(draft) ?? true); final currentList = _listTitle(draft.taskListId); @@ -200,6 +225,7 @@ class _TaskDetailsEditorState extends State { YaruListTile.square( title: TextField( controller: _titleController, + enabled: _canWrite, decoration: busyMaxGroupedTextFieldDecoration( context, labelText: l10n.title, @@ -219,79 +245,99 @@ class _TaskDetailsEditorState extends State { filled: true, children: [_listRow(draft, listValue)], ), - BusyMaxGroupedList( - title: l10n.dueGroup, - filled: true, - children: [ - if (_supportsScheduledTimeMode) - BusyMaxTimeModeRow( - allDay: scheduledAllDay, + if (widget.capabilities.supportsDueDate) + BusyMaxGroupedList( + title: l10n.dueGroup, + filled: true, + children: [ + if (_supportsScheduledTimeMode) + IgnorePointer( + ignoring: !_canWrite, + child: BusyMaxTimeModeRow( + allDay: scheduledAllDay, + onChanged: (value) => + _setScheduledAllDay(draft, value), + ), + ), + DesktopDateValueRow( + label: l10n.dueDate, + date: draft.dueDate, + enabled: _canWrite, onChanged: (value) => - _setScheduledAllDay(draft, value), - ), - DesktopDateValueRow( - label: l10n.dueDate, - date: draft.dueDate, - onChanged: (value) => - _updateDraft(draft.copyWith(dueDate: value)), - useNativePicker: widget.useNativeDatePicker, - onClear: () => _updateDraft( - draft.copyWith( - dueDate: null, - microsoftDueTime: - widget.capabilities.supportsDueTime - ? null - : draft.microsoftDueTime, + _updateDraft(draft.copyWith(dueDate: value)), + useNativePicker: widget.useNativeDatePicker, + onClear: () => unawaited( + _clearScheduledDate(draft, due: true), ), ), - ), - if (widget.capabilities.supportsDueTime && - !scheduledAllDay) - DesktopTimeValueRow( - label: l10n.dueTime, - time: draft.microsoftDueTime, - onChanged: (value) => _updateDraft( - draft.copyWith(microsoftDueTime: value), - ), - timeZone: draft.microsoftDueTimeZone, - onTimeZoneChanged: (value) => _updateDraft( - draft.copyWith(microsoftDueTimeZone: value), - ), - onValidityChanged: (valid) => _setTimeFieldValidity( - _TaskTimeField.due, - valid, + if (widget.capabilities.supportsDueTime && + !scheduledAllDay) + DesktopTimeValueRow( + label: l10n.dueTime, + time: draft.microsoftDueTime, + enabled: _canWrite, + onChanged: (value) => _updateDraft( + draft.copyWith(microsoftDueTime: value), + ), + timeZone: draft.microsoftDueTimeZone, + onTimeZoneChanged: (value) => _updateDraft( + draft.copyWith(microsoftDueTimeZone: value), + ), + onValidityChanged: (valid) => + _setTimeFieldValidity( + _TaskTimeField.due, + valid, + ), + useNativePicker: widget.useNativeDatePicker, ), - useNativePicker: widget.useNativeDatePicker, - ), - ], - ), + ], + ), if (widget.capabilities.supportsStartDateTime) BusyMaxGroupedList( title: l10n.startGroup, filled: true, children: _startRows(draft, scheduledAllDay), ), - if (widget.capabilities.supportsReminderDateTime) + if (scheduleIssue != TaskScheduleIssue.none) + _taskScheduleError(scheduleIssue), + if (_supportsIcalFields) + IcalTaskFieldsEditor( + draft: draft, + capabilities: widget.capabilities, + enabled: _canWrite, + useNativeDatePicker: widget.useNativeDatePicker, + dialogBarrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + onChanged: _updateDraft, + ), + if (widget.capabilities.supportsReminderDateTime && + !widget.capabilities.supportsMultipleReminders) BusyMaxGroupedList( title: l10n.reminderGroup, filled: true, children: _reminderRows(draft), ), - if (widget.capabilities.supportsRecurrence) + if (widget.capabilities.supportsRecurrence && + !widget.capabilities.supportsAdvancedRecurrence) BusyMaxGroupedList( filled: true, children: [_repeatRow(draft)], ), - if (widget.capabilities.supportsImportance || + if ((widget.capabilities.supportsImportance && + !widget.capabilities.supportsIcalPriority) || widget.capabilities.supportsCategories) BusyMaxGroupedList( title: l10n.organizationSection, filled: true, children: [ - if (widget.capabilities.supportsImportance) + if (widget.capabilities.supportsImportance && + !widget.capabilities.supportsIcalPriority) _importanceRow(draft), if (widget.capabilities.supportsCategories) - _categoriesRow(draft), + IgnorePointer( + ignoring: !_canWrite, + child: _categoriesRow(draft), + ), ], ), BusyMaxGroupedList( @@ -300,6 +346,7 @@ class _TaskDetailsEditorState extends State { YaruListTile.square( title: TextField( controller: _notesController, + enabled: _canWrite, minLines: 3, maxLines: 5, decoration: busyMaxGroupedTextFieldDecoration( @@ -315,27 +362,50 @@ class _TaskDetailsEditorState extends State { ), if (widget.showAdvancedActions && widget.capabilities.supportsTaskHierarchy) + _subtasksSection(), + if (widget.showAdvancedActions && + (widget.capabilities.supportsTaskReparenting || + widget.capabilities.supportsDuplicate || + widget.capabilities.supportsNativeExport)) BusyMaxGroupedList( title: l10n.advancedSection, filled: true, children: [ - BusyMaxActionRow( - title: l10n.createSubtask, - leading: Icon( - BusyMaxGlyphs.subdirectoryFor( - Directionality.of(context), - ), + if (widget.capabilities.supportsTaskReparenting) + BusyMaxActionRow( + title: l10n.moveToTop, + leading: const Icon(Icons.vertical_align_top), + enabled: widget.capabilities.canUpdateTasks, + onTap: widget.capabilities.canUpdateTasks + ? widget.onMoveToTop + : null, + ), + if (widget.capabilities.supportsDuplicate) + BusyMaxActionRow( + title: l10n.duplicateTask, + leading: const Icon(Icons.copy_outlined), + enabled: + widget.capabilities.canCreateTasks && + widget.onDuplicate != null, + onTap: + widget.capabilities.canCreateTasks && + widget.onDuplicate != null + ? () => unawaited(widget.onDuplicate!()) + : null, + ), + if (widget.capabilities.supportsNativeExport) + BusyMaxActionRow( + title: l10n.export, + leading: const Icon(Icons.file_download_outlined), + enabled: widget.onExport != null, + onTap: widget.onExport == null + ? null + : () => unawaited(widget.onExport!()), ), - onTap: _createSubtask, - ), - BusyMaxActionRow( - title: l10n.moveToTop, - leading: const Icon(Icons.vertical_align_top), - onTap: widget.onMoveToTop, - ), ], ), - if (widget.showDeleteAction) ...[ + if (widget.showDeleteAction && + widget.capabilities.canDeleteTasks) ...[ const SizedBox(height: BusyMaxSpacing.md), BusyMaxGroupedList( filled: true, @@ -372,6 +442,7 @@ class _TaskDetailsEditorState extends State { KeyEventResult _handleEditorKeyEvent(FocusNode node, KeyEvent event) { if (event is! KeyDownEvent || !widget.showDeleteAction || + !widget.capabilities.canDeleteTasks || _isEditableTextFocused()) { return KeyEventResult.ignored; } @@ -400,6 +471,23 @@ class _TaskDetailsEditorState extends State { widget.onAccountSelected != null; } + bool get _canWrite => widget.isCreate + ? widget.capabilities.canCreateTasks + : widget.capabilities.canUpdateTasks; + + bool get _supportsIcalFields => + widget.capabilities.supportsIcalPriority || + widget.capabilities.supportsPercentComplete || + widget.capabilities.supportsTaskStatus || + widget.capabilities.supportsCompletedDateTime || + widget.capabilities.supportsLocation || + widget.capabilities.supportsUrl || + widget.capabilities.supportsClassification || + widget.capabilities.supportsMultipleReminders || + widget.capabilities.supportsAdvancedRecurrence || + widget.capabilities.supportsPinning || + widget.capabilities.supportsSubtaskVisibility; + Widget _accountRow() { final l10n = context.l10n; final labelFor = widget.accountLabelFor!; @@ -445,6 +533,7 @@ class _TaskDetailsEditorState extends State { subtitle: widget.accountLabel, values: [for (final list in widget.taskLists) list.id], selected: draft.taskListId, + enabled: widget.isCreate || _canWrite, labelFor: (value) => _listTitle(value) ?? l10n.noneValue, onSelected: (value) => _updateDraft(draft.copyWith(taskListId: value)), ); @@ -456,17 +545,17 @@ class _TaskDetailsEditorState extends State { DesktopDateValueRow( label: l10n.startDate, date: draft.microsoftStartDate, + enabled: _canWrite, onChanged: (value) => _updateDraft(draft.copyWith(microsoftStartDate: value)), useNativePicker: widget.useNativeDatePicker, - onClear: () => _updateDraft( - draft.copyWith(microsoftStartDate: null, microsoftStartTime: null), - ), + onClear: () => unawaited(_clearScheduledDate(draft, due: false)), ), if (!scheduledAllDay) DesktopTimeValueRow( label: l10n.startTime, time: draft.microsoftStartTime, + enabled: _canWrite, onChanged: (value) => _updateDraft(draft.copyWith(microsoftStartTime: value)), timeZone: draft.microsoftStartTimeZone, @@ -479,6 +568,137 @@ class _TaskDetailsEditorState extends State { ]; } + Future _clearScheduledDate( + TaskDetailsDraft draft, { + required bool due, + }) async { + final relatedIndexes = []; + for (var index = 0; index < draft.alarms.length; index += 1) { + final alarm = draft.alarms[index]; + if (alarm.relativeOffset != null && alarm.isRelatedToDue == due) { + relatedIndexes.add(index); + } + } + + var alarms = draft.alarms; + if (relatedIndexes.isNotEmpty) { + final reference = draft.reminderReferenceUtc( + due: due, + localTimeZone: widget.localTimeZone, + ); + final choice = await showBusyMaxModalDialog<_RelatedReminderChoice>( + context, + barrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + barrierDismissible: false, + builder: (dialogContext) => BusyMaxDialogShell( + title: dialogContext.l10n.relatedRemindersTitle, + actions: [ + BusyMaxPushButton.standard( + autofocus: true, + onPressed: () => Navigator.pop(dialogContext), + child: Text(dialogContext.l10n.cancel), + ), + BusyMaxPushButton.destructive( + context: dialogContext, + onPressed: () => + Navigator.pop(dialogContext, _RelatedReminderChoice.discard), + child: Text(dialogContext.l10n.discardRelatedReminders), + ), + BusyMaxPushButton.suggested( + onPressed: reference == null + ? null + : () => Navigator.pop( + dialogContext, + _RelatedReminderChoice.keepAbsolute, + ), + child: Text(dialogContext.l10n.keepRelatedReminders), + ), + ], + children: [ + Text( + dialogContext.l10n.relatedRemindersDescription( + relatedIndexes.length, + ), + ), + ], + ), + ); + if (!mounted || choice == null) return; + final related = relatedIndexes.toSet(); + alarms = switch (choice) { + _RelatedReminderChoice.discard => [ + for (var index = 0; index < draft.alarms.length; index += 1) + if (!related.contains(index)) draft.alarms[index], + ], + _RelatedReminderChoice.keepAbsolute => [ + for (var index = 0; index < draft.alarms.length; index += 1) + if (related.contains(index)) + draft.alarms[index].withAbsoluteTrigger( + reference!.add(draft.alarms[index].relativeOffset!), + ) + else + draft.alarms[index], + ], + }; + } + + final noOtherDate = due + ? draft.microsoftStartDate == null + : draft.dueDate == null; + _updateDraft( + due + ? draft.copyWith( + dueDate: null, + microsoftDueTime: widget.capabilities.supportsDueTime + ? null + : draft.microsoftDueTime, + recurrenceJson: noOtherDate ? null : draft.recurrenceJson, + alarms: alarms, + ) + : draft.copyWith( + microsoftStartDate: null, + microsoftStartTime: null, + recurrenceJson: noOtherDate ? null : draft.recurrenceJson, + alarms: alarms, + ), + ); + } + + Widget _taskScheduleError(TaskScheduleIssue issue) { + final message = switch (issue) { + TaskScheduleIssue.dueBeforeStart => context.l10n.taskDueBeforeStart, + TaskScheduleIssue.mixedTimeModes => + context.l10n.taskStartDueTimeModeMismatch, + TaskScheduleIssue.none => '', + }; + final color = Theme.of(context).colorScheme.error; + return Padding( + key: const ValueKey('task-schedule-error'), + padding: const EdgeInsets.fromLTRB( + BusyMaxSpacing.lg, + BusyMaxSpacing.xs, + BusyMaxSpacing.lg, + BusyMaxSpacing.sm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.error_outline, size: 16, color: color), + const SizedBox(width: BusyMaxSpacing.xs), + Expanded( + child: Text( + message, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: color), + ), + ), + ], + ), + ); + } + bool get _supportsScheduledTimeMode { return widget.capabilities.supportsDueTime || widget.capabilities.supportsStartDateTime; @@ -541,9 +761,16 @@ class _TaskDetailsEditorState extends State { leading: const Icon(YaruIcons.repeat), values: options.keys.toList(), selected: type, + enabled: _canWrite, labelFor: (value) => options[value] ?? l10n.repeatNone, onSelected: (value) => _updateDraft( - draft.copyWith(recurrenceJson: _recurrenceJsonFor(value, draft)), + draft.copyWith( + recurrenceJson: _recurrenceJsonFor( + value, + draft, + dav: _editingTask.davCollectionId != null, + ), + ), ), ); } @@ -560,6 +787,7 @@ class _TaskDetailsEditorState extends State { leading: const Icon(YaruIcons.task_important), values: labels.keys.toList(), selected: draft.importance, + enabled: _canWrite, labelFor: (value) => labels[value] ?? l10n.importanceNormal, onSelected: (value) => _updateDraft(draft.copyWith(importance: value)), ); @@ -622,6 +850,9 @@ class _TaskDetailsEditorState extends State { final draft = _draft; if (draft == null || _saving || + !_canWrite || + draft.scheduleIssue != TaskScheduleIssue.none || + !draft.hasValidTaskUrl || !_timeFieldsAreValid(draft, _isScheduledAllDay(draft))) { return; } @@ -697,7 +928,148 @@ class _TaskDetailsEditorState extends State { } } + Widget _subtasksSection() { + final l10n = context.l10n; + final hierarchy = widget.hierarchy; + return BusyMaxGroupedList( + title: l10n.subtasks, + filled: true, + children: [ + if (hierarchy.parent case final parent?) + BusyMaxActionRow( + key: ValueKey('task-parent-${parent.id}'), + title: parent.title, + subtitle: l10n.parent, + leading: const Icon(Icons.account_tree_outlined), + trailing: Icon( + BusyMaxGlyphs.chevronForwardFor(Directionality.of(context)), + ), + enabled: widget.onHierarchyTaskSelected != null, + onTap: widget.onHierarchyTaskSelected == null + ? null + : () => unawaited(widget.onHierarchyTaskSelected!(parent)), + ), + for (final subtask in hierarchy.subtasks) _subtaskRow(subtask), + BusyMaxActionRow( + key: const ValueKey('create-subtask-action'), + title: l10n.createSubtask, + leading: Icon( + BusyMaxGlyphs.subdirectoryFor(Directionality.of(context)), + ), + enabled: widget.capabilities.canCreateTasks && !_creatingSubtask, + onTap: widget.capabilities.canCreateTasks && !_creatingSubtask + ? _createSubtask + : null, + ), + ], + ); + } + + Widget _subtaskRow(TaskSubtaskEntity subtask) { + final task = subtask.task; + final canToggle = + widget.capabilities.canUpdateTasks && + widget.onSubtaskCompletionChanged != null; + final title = Text( + subtask.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + decoration: subtask.completed ? TextDecoration.lineThrough : null, + color: subtask.completed + ? Theme.of(context).colorScheme.onSurfaceVariant + : Theme.of(context).colorScheme.onSurface, + ), + ); + return BusyMaxActionRow( + key: ValueKey('subtask-${subtask.kind.name}-${subtask.id}'), + title: subtask.title, + titleWidget: title, + leading: Icon(BusyMaxGlyphs.subdirectoryFor(Directionality.of(context))), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + YaruCheckbox( + value: subtask.completed, + onChanged: !canToggle + ? null + : (value) => unawaited( + widget.onSubtaskCompletionChanged!(subtask, value ?? false), + ), + ), + if (task != null) + Icon(BusyMaxGlyphs.chevronForwardFor(Directionality.of(context))) + else if (widget.onChecklistSubtaskRenamed != null || + widget.onChecklistSubtaskDeleted != null) + BusyMaxMenuButton<_ChecklistSubtaskAction>( + tooltip: MaterialLocalizations.of(context).moreButtonTooltip, + onSelected: (action) => + unawaited(_handleChecklistSubtaskAction(subtask, action)), + entries: [ + if (widget.onChecklistSubtaskRenamed != null) + BusyMaxMenuEntry( + value: _ChecklistSubtaskAction.rename, + label: context.l10n.rename, + icon: Icons.edit_outlined, + ), + if (widget.onChecklistSubtaskDeleted != null) + BusyMaxMenuEntry( + value: _ChecklistSubtaskAction.delete, + label: context.l10n.delete, + icon: YaruIcons.trash, + destructive: true, + ), + ], + ), + ], + ), + enabled: task == null || widget.onHierarchyTaskSelected != null, + onTap: task == null || widget.onHierarchyTaskSelected == null + ? null + : () => unawaited(widget.onHierarchyTaskSelected!(task)), + ); + } + + Future _handleChecklistSubtaskAction( + TaskSubtaskEntity subtask, + _ChecklistSubtaskAction action, + ) async { + switch (action) { + case _ChecklistSubtaskAction.rename: + final title = await showBusyMaxTextPrompt( + context, + title: context.l10n.rename, + label: context.l10n.title, + actionLabel: context.l10n.save, + initialValue: subtask.title, + barrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + ); + final normalized = title?.trim(); + if (normalized == null || + normalized.isEmpty || + normalized == subtask.title) { + return; + } + await widget.onChecklistSubtaskRenamed?.call(subtask, normalized); + case _ChecklistSubtaskAction.delete: + final confirmed = await showBusyMaxConfirm( + context, + title: context.l10n.delete, + message: context.l10n.deleteTaskConfirmation(subtask.title), + confirmLabel: context.l10n.delete, + destructive: true, + barrierColor: widget.dialogBarrierColor, + headerBarService: widget.headerBarService, + ); + if (confirmed) { + await widget.onChecklistSubtaskDeleted?.call(subtask); + } + } + } + Future _createSubtask() async { + if (!widget.capabilities.canCreateTasks || _creatingSubtask) return; final title = await showBusyMaxTextPrompt( context, title: context.l10n.newSubtask, @@ -709,11 +1081,16 @@ class _TaskDetailsEditorState extends State { if (title == null || title.trim().isEmpty) { return; } - widget.onCreateSubtask(title.trim()); + setState(() => _creatingSubtask = true); + try { + await widget.onCreateSubtask(title.trim()); + } finally { + if (mounted) setState(() => _creatingSubtask = false); + } } Future _deleteTask() async { - if (_confirmingDelete) { + if (_confirmingDelete || !widget.capabilities.canDeleteTasks) { return; } _confirmingDelete = true; @@ -840,14 +1217,17 @@ class _TaskDetailsEditorState extends State { ], ), ), - onTap: () => _updateDraft( - draft.copyWith( - microsoftReminderEnabled: true, - microsoftReminderDate: - draft.dueDate ?? encodeGoogleDateOnly(DateTime.now()), - microsoftReminderTime: '09:00', - ), - ), + enabled: _canWrite, + onTap: _canWrite + ? () => _updateDraft( + draft.copyWith( + microsoftReminderEnabled: true, + microsoftReminderDate: + draft.dueDate ?? encodeGoogleDateOnly(DateTime.now()), + microsoftReminderTime: '09:00', + ), + ) + : null, ), ]; } @@ -856,6 +1236,7 @@ class _TaskDetailsEditorState extends State { DesktopDateValueRow( label: l10n.reminderDate, date: draft.microsoftReminderDate, + enabled: _canWrite, onChanged: (value) => _updateDraft(draft.copyWith(microsoftReminderDate: value)), useNativePicker: widget.useNativeDatePicker, @@ -865,6 +1246,7 @@ class _TaskDetailsEditorState extends State { DesktopTimeValueRow( label: l10n.reminderTime, time: draft.microsoftReminderTime, + enabled: _canWrite, onChanged: (value) => _updateDraft(draft.copyWith(microsoftReminderTime: value)), timeZone: draft.microsoftReminderTimeZone, @@ -877,21 +1259,26 @@ class _TaskDetailsEditorState extends State { BusyMaxActionRow( title: l10n.removeReminder, leading: const Icon(YaruIcons.window_close), - onTap: () { - _invalidTimeFields.remove(_TaskTimeField.reminder); - _updateDraft( - draft.copyWith( - microsoftReminderEnabled: false, - microsoftReminderDate: null, - microsoftReminderTime: null, - ), - ); - }, + enabled: _canWrite, + onTap: _canWrite + ? () { + _invalidTimeFields.remove(_TaskTimeField.reminder); + _updateDraft( + draft.copyWith( + microsoftReminderEnabled: false, + microsoftReminderDate: null, + microsoftReminderTime: null, + ), + ); + } + : null, ), ]; } } +enum _ChecklistSubtaskAction { rename, delete } + TextStyle? _taskEditorProminentActionStyle( BuildContext context, { Color? color, @@ -904,6 +1291,8 @@ TextStyle? _taskEditorProminentActionStyle( enum _TaskTimeField { due, start, reminder } +enum _RelatedReminderChoice { discard, keepAbsolute } + class _TaskDetailsHeader extends StatelessWidget { const _TaskDetailsHeader({ required this.title, @@ -945,11 +1334,26 @@ bool _taskChanged(TaskEntity oldTask, TaskEntity newTask) { oldTask.notes != newTask.notes || oldTask.dueUtc != newTask.dueUtc || oldTask.microsoftDueDateTime != newTask.microsoftDueDateTime || + oldTask.microsoftDueTimeZone != newTask.microsoftDueTimeZone || oldTask.microsoftStartDateTime != newTask.microsoftStartDateTime || + oldTask.microsoftStartTimeZone != newTask.microsoftStartTimeZone || oldTask.microsoftReminderDateTime != newTask.microsoftReminderDateTime || + oldTask.microsoftReminderTimeZone != newTask.microsoftReminderTimeZone || + oldTask.microsoftIsReminderOn != newTask.microsoftIsReminderOn || oldTask.recurrenceJson != newTask.recurrenceJson || oldTask.importance != newTask.importance || - oldTask.categoriesJson != newTask.categoriesJson; + oldTask.categoriesJson != newTask.categoriesJson || + oldTask.icalPriority != newTask.icalPriority || + oldTask.percentComplete != newTask.percentComplete || + oldTask.providerStatus != newTask.providerStatus || + oldTask.completedUtc != newTask.completedUtc || + oldTask.taskLocation != newTask.taskLocation || + oldTask.taskUrl != newTask.taskUrl || + oldTask.taskClassification != newTask.taskClassification || + oldTask.taskPinned != newTask.taskPinned || + oldTask.taskHideSubtasks != newTask.taskHideSubtasks || + oldTask.taskHideCompletedSubtasks != newTask.taskHideCompletedSubtasks || + oldTask.taskAlarmsJson != newTask.taskAlarmsJson; } Map _repeatOptions(BuildContext context) { @@ -974,6 +1378,14 @@ String _recurrenceType(String? recurrenceJson) { if (pattern is Map) { return pattern['type']?.toString() ?? 'none'; } + final rules = decoded['rules']; + if (rules is List && rules.isNotEmpty) { + final rule = rules.first.toString().toUpperCase(); + if (rule.contains('FREQ=DAILY')) return 'daily'; + if (rule.contains('FREQ=WEEKLY')) return 'weekly'; + if (rule.contains('FREQ=MONTHLY')) return 'absoluteMonthly'; + if (rule.contains('FREQ=YEARLY')) return 'absoluteYearly'; + } } } on FormatException { return 'none'; @@ -981,11 +1393,40 @@ String _recurrenceType(String? recurrenceJson) { return 'none'; } -String? _recurrenceJsonFor(String type, TaskDetailsDraft draft) { +String? _recurrenceJsonFor( + String type, + TaskDetailsDraft draft, { + required bool dav, +}) { if (type == 'none') { return null; } final now = DateTime.now(); + final recurrenceStart = DateTime.tryParse(draft.dueDate ?? '') ?? now; + if (dav) { + final frequency = switch (type) { + 'daily' => 'DAILY', + 'weekly' => 'WEEKLY', + 'absoluteMonthly' => 'MONTHLY', + 'absoluteYearly' => 'YEARLY', + _ => 'DAILY', + }; + final parts = ['FREQ=$frequency', 'INTERVAL=1']; + if (type == 'weekly') { + parts.add('BYDAY=${_weekdayCode(recurrenceStart.weekday)}'); + } else if (type == 'absoluteMonthly') { + parts.add('BYMONTHDAY=${recurrenceStart.day}'); + } else if (type == 'absoluteYearly') { + parts + ..add('BYMONTH=${recurrenceStart.month}') + ..add('BYMONTHDAY=${recurrenceStart.day}'); + } + return jsonEncode({ + 'rules': [parts.join(';')], + 'dates': const [], + 'excludedDates': const [], + }); + } final pattern = switch (type) { 'daily' => {'type': 'daily', 'interval': 1}, 'weekly' => { @@ -1016,6 +1457,16 @@ String? _recurrenceJsonFor(String type, TaskDetailsDraft draft) { }); } +String _weekdayCode(int weekday) => switch (weekday) { + DateTime.monday => 'MO', + DateTime.tuesday => 'TU', + DateTime.wednesday => 'WE', + DateTime.thursday => 'TH', + DateTime.friday => 'FR', + DateTime.saturday => 'SA', + _ => 'SU', +}; + String _weekdayName(int weekday) { return switch (weekday) { DateTime.monday => 'monday', diff --git a/lib/src/features/tasks/presentation/task_details_pane.dart b/lib/src/features/tasks/presentation/task_details_pane.dart index 130ed80..9ee4c79 100644 --- a/lib/src/features/tasks/presentation/task_details_pane.dart +++ b/lib/src/features/tasks/presentation/task_details_pane.dart @@ -1,14 +1,17 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/app_bootstrap.dart'; import '../../../app/busymax_dialogs.dart'; import '../../../l10n/l10n.dart'; -import '../../../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import '../../accounts/data/accounts_repository.dart'; +import '../../schedule/presentation/schedule_item_exporter.dart'; import '../../sync/sync_auth_error.dart'; import '../../task_lists/data/task_lists_repository.dart'; import '../data/tasks_repository.dart'; @@ -58,7 +61,7 @@ class _TaskDetailsPaneState extends ConsumerState { TaskEntity? _lastTask; List _lastTaskLists = const []; AccountEntity? _lastAccount; - TaskProviderCapabilities? _lastCapabilities; + TaskCollectionCapabilities? _lastCapabilities; String? _lastLocalTimeZone; List _lastCategorySuggestions = const []; TasksRepository? _taskStreamRepository; @@ -66,6 +69,14 @@ class _TaskDetailsPaneState extends ConsumerState { String? _taskStreamTaskListId; String? _taskStreamTaskId; Stream? _taskStream; + TasksRepository? _hierarchyStreamRepository; + String? _hierarchyStreamTaskListId; + String? _hierarchyStreamTaskId; + Stream? _hierarchyStream; + TaskHierarchySnapshot _lastHierarchy = const TaskHierarchySnapshot( + parent: null, + subtasks: [], + ); TasksRepository? _categorySuggestionsRepository; Stream>? _categorySuggestionsStream; TaskListsRepository? _listsStreamRepository; @@ -88,6 +99,11 @@ class _TaskDetailsPaneState extends ConsumerState { @override void didUpdateWidget(covariant TaskDetailsPane oldWidget) { super.didUpdateWidget(oldWidget); + if (oldWidget.accountId == widget.accountId && + oldWidget.taskListId == widget.taskListId && + oldWidget.taskId == widget.taskId) { + return; + } if (_matchesEffectiveSelection( widget.accountId, widget.taskListId, @@ -137,8 +153,11 @@ class _TaskDetailsPaneState extends ConsumerState { ? cachedAccount : null); final taskStream = _watchTask(repository); + final hierarchyStream = _watchTaskHierarchy(repository); final listsStream = _watchTaskLists(listsRepository); final categorySuggestionsStream = _watchCategorySuggestions(repository); + final davCollections = + ref.watch(davCollectionsStreamProvider).valueOrNull ?? const []; if (account == null) { if (accounts.hasValue && !accounts.isLoading) { @@ -148,7 +167,24 @@ class _TaskDetailsPaneState extends ConsumerState { } return const SizedBox.shrink(); } - final capabilities = capabilitiesForProvider(account.provider); + final davCapabilities = switch (account.provider) { + BusyProvider.appleICloud || BusyProvider.nextcloud => + ref + .watch( + davTaskCollectionCapabilitiesProvider(( + accountId: _effectiveAccountId, + taskListId: _effectiveTaskListId, + )), + ) + .valueOrNull, + BusyProvider.google || BusyProvider.microsoft => null, + }; + final capabilities = switch (account.provider) { + BusyProvider.appleICloud || + BusyProvider.nextcloud => davCapabilities ?? noTaskCollectionCapabilities, + BusyProvider.google || BusyProvider.microsoft => + adapterDefaultTaskCapabilities(account.provider), + }; return StreamBuilder( stream: taskStream, @@ -164,6 +200,7 @@ class _TaskDetailsPaneState extends ConsumerState { localTimeZone: _lastLocalTimeZone ?? localTimeZone, account: cachedAccount ?? account, categorySuggestions: _lastCategorySuggestions, + hierarchy: _lastHierarchy, ); } return const SizedBox.shrink(); @@ -181,30 +218,57 @@ class _TaskDetailsPaneState extends ConsumerState { ); return const SizedBox.shrink(); } + var effectiveCapabilities = + task.recurrenceIdKey != null && + !capabilities.supportsRecurringTaskOccurrenceEditing + ? capabilities.asReadOnly() + : capabilities; + if (task.davCollectionId != null) { + final collection = davCollections + .where((item) => item.id == task.davCollectionId) + .firstOrNull; + if (collection?.shared ?? false) { + final classification = + task.taskClassification?.trim().toUpperCase() ?? 'PUBLIC'; + effectiveCapabilities = classification == 'PUBLIC' + ? effectiveCapabilities.withoutClassificationEditing() + : effectiveCapabilities.asReadOnly(); + } + } - return StreamBuilder>( - stream: listsStream, - builder: (context, listsSnapshot) { - final taskLists = listsSnapshot.data ?? const []; - return StreamBuilder>( - stream: categorySuggestionsStream, - builder: (context, categorySnapshot) { - final categorySuggestions = - categorySnapshot.data ?? _lastCategorySuggestions; - _lastTask = task; - _lastTaskLists = taskLists; - _lastAccount = account; - _lastCapabilities = capabilities; - _lastLocalTimeZone = localTimeZone; - _lastCategorySuggestions = categorySuggestions; - return _buildEditor( - repository: repository, - task: task, - taskLists: taskLists, - capabilities: capabilities, - localTimeZone: localTimeZone, - account: account, - categorySuggestions: categorySuggestions, + return StreamBuilder( + stream: hierarchyStream, + initialData: _lastHierarchy, + builder: (context, hierarchySnapshot) { + final hierarchy = hierarchySnapshot.data ?? _lastHierarchy; + return StreamBuilder>( + stream: listsStream, + builder: (context, listsSnapshot) { + final taskLists = + listsSnapshot.data ?? const []; + return StreamBuilder>( + stream: categorySuggestionsStream, + builder: (context, categorySnapshot) { + final categorySuggestions = + categorySnapshot.data ?? _lastCategorySuggestions; + _lastTask = task; + _lastTaskLists = taskLists; + _lastAccount = account; + _lastCapabilities = effectiveCapabilities; + _lastLocalTimeZone = localTimeZone; + _lastCategorySuggestions = categorySuggestions; + _lastHierarchy = hierarchy; + return _buildEditor( + repository: repository, + task: task, + taskLists: taskLists, + capabilities: effectiveCapabilities, + localTimeZone: localTimeZone, + account: account, + categorySuggestions: categorySuggestions, + hierarchy: hierarchy, + ); + }, ); }, ); @@ -272,10 +336,11 @@ class _TaskDetailsPaneState extends ConsumerState { required TasksRepository repository, required TaskEntity task, required List taskLists, - required TaskProviderCapabilities capabilities, + required TaskCollectionCapabilities capabilities, required String localTimeZone, required AccountEntity account, required List categorySuggestions, + required TaskHierarchySnapshot hierarchy, }) { return TaskDetailsEditor( task: task, @@ -288,12 +353,21 @@ class _TaskDetailsPaneState extends ConsumerState { unawaited(_refreshTask(repository, task)); }, onSave: (draft, patch) => _saveDraft(repository, task, draft, patch), - onCreateSubtask: (title) { - unawaited(_createSubtask(repository, task, title)); - }, + hierarchy: hierarchy, + onCreateSubtask: (title) => _createSubtask(repository, task, title), + onHierarchyTaskSelected: (selectedTask) => + _selectHierarchyTask(selectedTask), + onSubtaskCompletionChanged: (subtask, completed) => + _setSubtaskCompleted(repository, task, subtask, completed), + onChecklistSubtaskRenamed: (subtask, title) => + _renameChecklistSubtask(repository, task, subtask, title), + onChecklistSubtaskDeleted: (subtask) => + _deleteChecklistSubtask(repository, task, subtask), onMoveToTop: () { unawaited(_moveToTop(repository, task)); }, + onDuplicate: () => _duplicateTask(repository, task), + onExport: () => _exportTask(repository, task), onDelete: () async { await repository.deleteTask(task.taskListId, task.id); await widget.onTaskMutationCommitted?.call(task.accountId); @@ -313,11 +387,102 @@ class _TaskDetailsPaneState extends ConsumerState { TaskEntity task, String title, ) async { - await repository.createTask( - task.taskListId, - TaskCreateInput(title: title, parentTaskId: task.id), + try { + await repository.createSubtask( + taskListId: task.taskListId, + parentTaskId: task.id, + title: title, + ); + await widget.onTaskMutationCommitted?.call(task.accountId); + } on Object catch (error) { + _showTaskMutationError(error); + } + } + + Future _selectHierarchyTask(TaskEntity task) { + return _confirmSelectionChange( + accountId: task.accountId, + taskListId: task.taskListId, + taskId: task.id, + ); + } + + Future _setSubtaskCompleted( + TasksRepository repository, + TaskEntity parent, + TaskSubtaskEntity subtask, + bool completed, + ) async { + try { + if (subtask.kind == TaskSubtaskKind.task) { + final task = subtask.task!; + await repository.patchTask( + task.taskListId, + task.id, + TaskPatchInput({ + 'status': completed ? 'completed' : 'needsAction', + 'completed': completed + ? DateTime.now().toUtc().toIso8601String() + : null, + }), + ); + } else { + await repository.patchChecklistSubtask( + taskListId: parent.taskListId, + parentTaskId: parent.id, + checklistItemId: subtask.id, + completed: completed, + ); + } + await widget.onTaskMutationCommitted?.call(parent.accountId); + } on Object catch (error) { + _showTaskMutationError(error); + } + } + + Future _renameChecklistSubtask( + TasksRepository repository, + TaskEntity parent, + TaskSubtaskEntity subtask, + String title, + ) async { + try { + await repository.patchChecklistSubtask( + taskListId: parent.taskListId, + parentTaskId: parent.id, + checklistItemId: subtask.id, + title: title, + ); + await widget.onTaskMutationCommitted?.call(parent.accountId); + } on Object catch (error) { + _showTaskMutationError(error); + } + } + + Future _deleteChecklistSubtask( + TasksRepository repository, + TaskEntity parent, + TaskSubtaskEntity subtask, + ) async { + try { + await repository.deleteChecklistSubtask( + taskListId: parent.taskListId, + parentTaskId: parent.id, + checklistItemId: subtask.id, + ); + await widget.onTaskMutationCommitted?.call(parent.accountId); + } on Object catch (error) { + _showTaskMutationError(error); + } + } + + void _showTaskMutationError(Object error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.syncFailed(syncFailureMessage(error))), + ), ); - await widget.onTaskMutationCommitted?.call(task.accountId); } Future _moveToTop(TasksRepository repository, TaskEntity task) async { @@ -327,6 +492,56 @@ class _TaskDetailsPaneState extends ConsumerState { await widget.onTaskMutationCommitted?.call(task.accountId); } + Future _duplicateTask( + TasksRepository repository, + TaskEntity task, + ) async { + try { + await repository.duplicateTask(task.taskListId, task.id); + await widget.onTaskMutationCommitted?.call(task.accountId); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.taskDuplicated))); + } on Object catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.taskDuplicateFailed(syncFailureMessage(error)), + ), + ), + ); + } + } + + Future _exportTask(TasksRepository repository, TaskEntity task) async { + try { + final raw = await repository.nativeTaskExport(task.taskListId, task.id); + if (raw == null) { + throw StateError('The native iCalendar task data is unavailable.'); + } + final file = await exportICalendarWithSaveDialog( + suggestedName: taskExportFileName( + title: task.title, + dueDate: task.dueUtc, + ), + calendarData: raw, + ); + if (file == null || !mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.exportedFile(file.path))), + ); + } on Object catch (error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.exportFailed(syncFailureMessage(error))), + ), + ); + } + } + void _setEditorDirty(bool dirty) { if (!mounted) { return; @@ -353,6 +568,7 @@ class _TaskDetailsPaneState extends ConsumerState { _effectiveAccountId = accountId; _effectiveTaskListId = taskListId; _effectiveTaskId = taskId; + _lastHierarchy = const TaskHierarchySnapshot(parent: null, subtasks: []); } TasksRepository _tasksRepository(WidgetRef ref, String accountId) { @@ -390,6 +606,16 @@ class _TaskDetailsPaneState extends ConsumerState { _matchesEffectiveSelection(accountId, taskListId, taskId)) { return; } + if (!_editorDirty) { + setState(() { + _applyEffectiveSelection( + accountId: accountId, + taskListId: taskListId, + taskId: taskId, + ); + }); + return; + } _confirmingTaskSwitch = true; final previousTask = _lastTask; final discard = await showBusyMaxConfirm( @@ -434,6 +660,23 @@ class _TaskDetailsPaneState extends ConsumerState { return _taskStream!; } + Stream _watchTaskHierarchy( + TasksRepository repository, + ) { + if (!identical(_hierarchyStreamRepository, repository) || + _hierarchyStreamTaskListId != _effectiveTaskListId || + _hierarchyStreamTaskId != _effectiveTaskId || + _hierarchyStream == null) { + _hierarchyStreamRepository = repository; + _hierarchyStreamTaskListId = _effectiveTaskListId; + _hierarchyStreamTaskId = _effectiveTaskId; + _hierarchyStream = repository + .watchTaskHierarchy(_effectiveTaskListId, _effectiveTaskId) + .asBroadcastStream(); + } + return _hierarchyStream!; + } + Stream> _watchTaskLists( TaskListsRepository listsRepository, ) { @@ -483,10 +726,8 @@ String _accountEditorLabel(BuildContext context, AccountEntity account) { return email; } - final providerAccountId = account.providerAccountId?.trim(); - if (providerAccountId != null && - providerAccountId.isNotEmpty && - providerAccountId.contains('@')) { + final providerAccountId = account.providerAccountId.trim(); + if (providerAccountId.isNotEmpty && providerAccountId.contains('@')) { return providerAccountId; } @@ -502,11 +743,13 @@ String _accountEditorLabel(BuildContext context, AccountEntity account) { return _providerEditorLabel(context, account.provider); } -String _providerEditorLabel(BuildContext context, TaskProvider provider) { +String _providerEditorLabel(BuildContext context, BusyProvider provider) { final l10n = context.l10n; return switch (provider) { - TaskProvider.google => l10n.googleTasksProvider, - TaskProvider.microsoft => l10n.microsoftTodoProvider, + BusyProvider.google => l10n.googleTasksProvider, + BusyProvider.microsoft => l10n.microsoftTodoProvider, + BusyProvider.appleICloud => 'Apple iCloud', + BusyProvider.nextcloud => 'Nextcloud Tasks', }; } diff --git a/lib/src/google_calendar/google_calendar_api_client.dart b/lib/src/google_calendar/google_calendar_api_client.dart index 9ec928b..382b370 100644 --- a/lib/src/google_calendar/google_calendar_api_client.dart +++ b/lib/src/google_calendar/google_calendar_api_client.dart @@ -6,7 +6,7 @@ import '../calendar_providers/calendar_mutation.dart'; import '../calendar_providers/calendar_provider_capabilities.dart'; import '../calendar_providers/calendar_sync_dto.dart'; import '../calendar_providers/cloud_calendar_client.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'google_calendar_errors.dart'; import 'google_calendar_mapper.dart'; import 'google_calendar_models.dart'; @@ -28,7 +28,7 @@ class GoogleCalendarApiClient implements CloudCalendarClient { final Future Function()? _unauthorizedRefreshProvider; @override - BusyProvider get provider => TaskProvider.google; + BusyProvider get provider => BusyProvider.google; @override CalendarProviderCapabilities get capabilities => diff --git a/lib/src/google_calendar/google_calendar_mapper.dart b/lib/src/google_calendar/google_calendar_mapper.dart index 237e41a..139d998 100644 --- a/lib/src/google_calendar/google_calendar_mapper.dart +++ b/lib/src/google_calendar/google_calendar_mapper.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import '../calendar_providers/calendar_mutation.dart'; import '../calendar_providers/calendar_sync_dto.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; CalendarSourceDto googleCalendarSourceFromJson(Map json) { final accessRole = json['accessRole']?.toString(); return CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: json['id']?.toString() ?? '', summary: json['summary']?.toString().trim().isNotEmpty == true ? json['summary']!.toString() @@ -37,7 +37,7 @@ CalendarEventDto googleCalendarEventFromJson( final startDate = start['date']?.toString(); final status = json['status']?.toString(); return CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: calendarId, providerEventId: json['id']?.toString() ?? '', providerRecurringEventId: json['recurringEventId']?.toString(), diff --git a/lib/src/google_tasks/api/google_tasks_api_client.dart b/lib/src/google_tasks/api/google_tasks_api_client.dart index 80c36f0..a4972f2 100644 --- a/lib/src/google_tasks/api/google_tasks_api_client.dart +++ b/lib/src/google_tasks/api/google_tasks_api_client.dart @@ -2,68 +2,13 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import '../../features/tasks/domain/task_remote_client.dart'; +import '../../features/tasks/domain/task_remote_models.dart'; import 'google_tasks_api_error.dart'; -import 'google_tasks_api_models.dart'; import 'google_tasks_api_paths.dart'; import 'google_tasks_json.dart'; -abstract interface class GoogleTasksApiClient { - Future deleteTaskList(String taskListId); - Future getTaskList(String taskListId); - Future createTaskList({required String title}); - Future listTaskListsPage({ - int maxResults = 1000, - String? pageToken, - }); - Future patchTaskList(String taskListId, TaskListPatch patch); - Future updateTaskList( - String taskListId, - TaskListPut replacement, - ); - - Future clearCompletedTasks(String taskListId); - Future deleteTask({required String taskListId, required String taskId}); - Future getTask({required String taskListId, required String taskId}); - Future createTask({ - required String taskListId, - String? parentTaskId, - String? previousSiblingTaskId, - required TaskCreate create, - }); - Future listTasksPage({ - required String taskListId, - DateTime? completedMax, - DateTime? completedMin, - DateTime? dueMax, - DateTime? dueMin, - int maxResults = 100, - String? pageToken, - bool showCompleted = true, - bool showDeleted = false, - bool showHidden = false, - DateTime? updatedMin, - bool showAssigned = false, - }); - Future moveTask({ - required String sourceTaskListId, - required String taskId, - String? parentTaskId, - String? previousSiblingTaskId, - String? destinationTaskListId, - }); - Future patchTask({ - required String taskListId, - required String taskId, - required TaskPatch patch, - }); - Future updateTask({ - required String taskListId, - required String taskId, - required TaskPut replacement, - }); -} - -class GoogleTasksRestApiClient implements GoogleTasksApiClient { +class GoogleTasksRestApiClient implements TaskRemoteClient { GoogleTasksRestApiClient({ required http.Client httpClient, required Uri baseUri, diff --git a/lib/src/google_tasks/api/google_tasks_api_error.dart b/lib/src/google_tasks/api/google_tasks_api_error.dart index b0a1085..34d1d3b 100644 --- a/lib/src/google_tasks/api/google_tasks_api_error.dart +++ b/lib/src/google_tasks/api/google_tasks_api_error.dart @@ -1,13 +1,18 @@ import 'dart:convert'; -class GoogleTasksApiError implements Exception { +import '../../features/tasks/domain/task_remote_error.dart'; + +class GoogleTasksApiError extends TaskRemoteError { const GoogleTasksApiError({ - required this.statusCode, - required this.message, - this.code, + required super.statusCode, + required super.message, + super.code, this.status, this.rawJson, - }); + }) : super( + retryable: statusCode == 429 || statusCode >= 500, + providerDetails: rawJson, + ); factory GoogleTasksApiError.fromResponse({ required int statusCode, @@ -48,10 +53,7 @@ class GoogleTasksApiError implements Exception { } } - final int statusCode; - final String? code; final String? status; - final String message; final Map? rawJson; @override diff --git a/lib/src/google_tasks/http/authenticated_http_client.dart b/lib/src/google_tasks/http/authenticated_http_client.dart index b64c162..79115ba 100644 --- a/lib/src/google_tasks/http/authenticated_http_client.dart +++ b/lib/src/google_tasks/http/authenticated_http_client.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import '../../core/logging/redacting_logger.dart'; -import '../oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import '../oauth/oauth_service.dart'; class AuthenticatedHttpClient extends http.BaseClient { diff --git a/lib/src/google_tasks/oauth/oauth_loopback_flow.dart b/lib/src/google_tasks/oauth/oauth_loopback_flow.dart index 981f279..44abf7c 100644 --- a/lib/src/google_tasks/oauth/oauth_loopback_flow.dart +++ b/lib/src/google_tasks/oauth/oauth_loopback_flow.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'pkce.dart'; const googleTasksOAuthScope = 'https://www.googleapis.com/auth/tasks'; diff --git a/lib/src/google_tasks/oauth/oauth_service.dart b/lib/src/google_tasks/oauth/oauth_service.dart index b356707..715e0d6 100644 --- a/lib/src/google_tasks/oauth/oauth_service.dart +++ b/lib/src/google_tasks/oauth/oauth_service.dart @@ -7,10 +7,11 @@ import 'package:logging/logging.dart'; import '../../config/build_config.dart'; import '../../core/logging/redacting_logger.dart'; +import '../../providers/busy_provider.dart'; import '../api/google_tasks_api_surface.dart'; import 'oauth_loopback_flow.dart'; -import 'oauth_models.dart'; -import 'oauth_token_store.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; abstract interface class OAuthGateway { Future get activeAccountId; @@ -36,7 +37,7 @@ class OAuthService implements OAuthGateway { OAuthService({ required BuildConfig config, required http.Client httpClient, - required OAuthTokenStore tokenStore, + required SecretStore tokenStore, required OAuthLoopbackFlow loopbackFlow, DateTime Function()? nowUtc, Duration authorizationRevocationTimeout = const Duration(seconds: 10), @@ -49,7 +50,7 @@ class OAuthService implements OAuthGateway { final BuildConfig _config; final http.Client _httpClient; - final OAuthTokenStore _tokenStore; + final SecretStore _tokenStore; final OAuthLoopbackFlow _loopbackFlow; final DateTime Function() _nowUtc; final Duration _authorizationRevocationTimeout; @@ -64,11 +65,11 @@ class OAuthService implements OAuthGateway { if (accountId == null) { return null; } - return _tokenStore.readTokenSet(accountId); + return _readTokenSet(accountId); } Future readTokenSet(String accountId) { - return _tokenStore.readTokenSet(accountId); + return _readTokenSet(accountId); } @override @@ -99,7 +100,7 @@ class OAuthService implements OAuthGateway { } Future validTokenForAccount(String accountId) async { - final tokenSet = await _tokenStore.readTokenSet(accountId); + final tokenSet = await _readTokenSet(accountId); if (tokenSet == null) { throw const OAuthException( 'OAuthMissingToken', @@ -140,7 +141,11 @@ class OAuthService implements OAuthGateway { fallbackScopeText: result.callback.scope, ); final accountId = deriveAccountId(tokenSet); - await _tokenStore.saveTokenSet(accountId, tokenSet); + await _tokenStore.saveOAuthTokenSet( + accountId, + BusyProvider.google, + tokenSet, + ); await _tokenStore.setActiveAccountId(accountId); return OAuthSignInResult(accountId: accountId, tokenSet: tokenSet); } @@ -213,7 +218,7 @@ class OAuthService implements OAuthGateway { } Future refreshTokenForAccount(String accountId) async { - final current = await _tokenStore.readTokenSet(accountId); + final current = await _readTokenSet(accountId); if (current == null || !current.canRefresh) { throw const OAuthException( 'OAuthRefreshFailed', @@ -223,7 +228,11 @@ class OAuthService implements OAuthGateway { try { final refreshed = await refreshToken(current); - await _tokenStore.saveTokenSet(accountId, refreshed); + await _tokenStore.saveOAuthTokenSet( + accountId, + BusyProvider.google, + refreshed, + ); return refreshed; } on OAuthException catch (error) { if (error is OAuthRefreshException && error.statusCode == 400) { @@ -290,7 +299,7 @@ class OAuthService implements OAuthGateway { @override Future revokeAuthorization(String accountId) async { - final tokenSet = await _tokenStore.readTokenSet(accountId); + final tokenSet = await _readTokenSet(accountId); final token = tokenSet?.refreshToken ?? tokenSet?.accessToken; if (token == null || token.isEmpty) { throw const OAuthException( @@ -329,7 +338,7 @@ class OAuthService implements OAuthGateway { final targetAccountId = accountId ?? await _tokenStore.readActiveAccountId(); if (targetAccountId != null) { - await _tokenStore.clearTokenSet(targetAccountId); + await _tokenStore.deleteCredential(targetAccountId); } if (targetAccountId == null || await _tokenStore.readActiveAccountId() == targetAccountId) { @@ -339,11 +348,19 @@ class OAuthService implements OAuthGateway { Future _clearAccountAfterInvalidRefresh(String accountId) async { final active = await _tokenStore.readActiveAccountId(); - await _tokenStore.clearTokenSet(accountId); + await _tokenStore.deleteCredential(accountId); if (active == accountId) { await _tokenStore.clearActiveAccount(); } } + + Future _readTokenSet(String accountId) async { + await _tokenStore.migrateLegacyOAuthCredential( + accountId, + BusyProvider.google, + ); + return _tokenStore.readOAuthTokenSet(accountId, BusyProvider.google); + } } class OAuthSignInResult { diff --git a/lib/src/google_tasks/oauth/oauth_token_store.dart b/lib/src/google_tasks/oauth/oauth_token_store.dart deleted file mode 100644 index d27a3df..0000000 --- a/lib/src/google_tasks/oauth/oauth_token_store.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter/services.dart'; -import 'package:logging/logging.dart'; - -import '../../core/logging/redacting_logger.dart'; -import 'oauth_models.dart'; - -abstract interface class OAuthTokenStore { - Future readActiveAccountId(); - - Future readTokenSet(String accountId); - - Future saveTokenSet(String accountId, OAuthTokenSet tokenSet); - - Future setActiveAccountId(String accountId); - - Future clearTokenSet(String accountId); - - Future clearActiveAccount(); -} - -class SecureOAuthTokenStore implements OAuthTokenStore { - SecureOAuthTokenStore(this._storage, {RedactingLogger? logger}) - : _logger = logger ?? RedactingLogger(Logger('SecureOAuthTokenStore')); - - final FlutterSecureStorage _storage; - final RedactingLogger _logger; - var _loggedRuntime = false; - - static const activeAccountKey = 'busymax.oauth.active_account_id'; - - @override - Future readActiveAccountId() => _read(activeAccountKey); - - @override - Future readTokenSet(String accountId) async { - final accessToken = await _read(_key(accountId, 'access_token')); - final expiresAtText = await _read(_key(accountId, 'expires_at_utc')); - if (accessToken == null || expiresAtText == null) { - return null; - } - - final refreshToken = await _read(_key(accountId, 'refresh_token')); - final tokenType = await _read(_key(accountId, 'token_type')); - final scopeText = await _read(_key(accountId, 'scope')); - final idToken = await _read(_key(accountId, 'id_token')); - - return OAuthTokenSet( - accessToken: accessToken, - refreshToken: refreshToken, - idToken: idToken, - expiresAtUtc: DateTime.parse(expiresAtText).toUtc(), - tokenType: tokenType ?? 'Bearer', - scopes: (scopeText ?? '') - .split(RegExp(r'\s+')) - .where((scope) => scope.isNotEmpty) - .toSet(), - ); - } - - @override - Future saveTokenSet(String accountId, OAuthTokenSet tokenSet) async { - await _write(_key(accountId, 'access_token'), tokenSet.accessToken); - if (tokenSet.refreshToken != null) { - await _write(_key(accountId, 'refresh_token'), tokenSet.refreshToken); - } - if (tokenSet.idToken != null) { - await _write(_key(accountId, 'id_token'), tokenSet.idToken); - } - await _write( - _key(accountId, 'expires_at_utc'), - tokenSet.expiresAtUtc.toUtc().toIso8601String(), - ); - await _write(_key(accountId, 'token_type'), tokenSet.tokenType); - await _write(_key(accountId, 'scope'), tokenSet.scopes.join(' ')); - } - - @override - Future setActiveAccountId(String accountId) { - return _write(activeAccountKey, accountId); - } - - @override - Future clearTokenSet(String accountId) async { - await _delete(_key(accountId, 'access_token')); - await _delete(_key(accountId, 'refresh_token')); - await _delete(_key(accountId, 'id_token')); - await _delete(_key(accountId, 'expires_at_utc')); - await _delete(_key(accountId, 'token_type')); - await _delete(_key(accountId, 'scope')); - } - - @override - Future clearActiveAccount() { - return _delete(activeAccountKey); - } - - String _key(String accountId, String name) => - 'busymax.oauth.$accountId.$name'; - - Future _read(String key) async { - _logRuntime(); - try { - return await _storage.read(key: key); - } on PlatformException catch (error) { - throw _secureStorageException('read', error); - } - } - - Future _write(String key, String? value) async { - _logRuntime(); - try { - await _storage.write(key: key, value: value); - } on PlatformException catch (error) { - throw _secureStorageException('write', error); - } - } - - Future _delete(String key) async { - _logRuntime(); - try { - await _storage.delete(key: key); - } on PlatformException catch (error) { - throw _secureStorageException('delete', error); - } - } - - void _logRuntime() { - if (_loggedRuntime) { - return; - } - _loggedRuntime = true; - _logger.info( - 'Secure token storage runtime: backend=flutter-secure-storage ' - 'snap=${_isRunningInSnap()} secret_backend=${_secretBackendLabel()}', - ); - } - - OAuthException _secureStorageException( - String operation, - PlatformException error, - ) { - _logger.warning( - 'Secure token storage $operation failed: ' - '${sanitizedFlutterSecureStorageError(error)}', - ); - return const OAuthException( - 'OAuthSecureStorageUnavailable', - secureTokenStorageUnavailableMessage, - ); - } -} - -const secureTokenStorageUnavailableMessage = - 'Secure token storage is locked. Unlock your system keyring and try again.'; - -String sanitizedFlutterSecureStorageError(PlatformException error) { - final message = redactForLog(error.message).replaceAll(RegExp(r'\s+'), ' '); - return 'domain=flutter_secure_storage_linux code=${error.code} ' - 'message=${message.trim()} details_type=${error.details.runtimeType}'; -} - -class InMemoryOAuthTokenStore implements OAuthTokenStore { - final _tokens = {}; - String? _activeAccountId; - - @override - Future clearActiveAccount() async { - _activeAccountId = null; - } - - @override - Future clearTokenSet(String accountId) async { - _tokens.remove(accountId); - } - - @override - Future readActiveAccountId() async => _activeAccountId; - - @override - Future readTokenSet(String accountId) async { - return _tokens[accountId]; - } - - @override - Future saveTokenSet(String accountId, OAuthTokenSet tokenSet) async { - _tokens[accountId] = tokenSet; - } - - @override - Future setActiveAccountId(String accountId) async { - _activeAccountId = accountId; - } -} - -bool _isRunningInSnap() => Platform.environment['SNAP']?.isNotEmpty ?? false; - -String _secretBackendLabel() { - final backend = Platform.environment['SECRET_BACKEND']; - if (backend == null || backend.isEmpty) { - return ''; - } - if (backend == 'file') { - return 'file'; - } - return ''; -} diff --git a/lib/src/microsoft_calendar/microsoft_calendar_api_client.dart b/lib/src/microsoft_calendar/microsoft_calendar_api_client.dart index 742cbf3..1fe760f 100644 --- a/lib/src/microsoft_calendar/microsoft_calendar_api_client.dart +++ b/lib/src/microsoft_calendar/microsoft_calendar_api_client.dart @@ -6,7 +6,7 @@ import '../calendar_providers/calendar_mutation.dart'; import '../calendar_providers/calendar_provider_capabilities.dart'; import '../calendar_providers/calendar_sync_dto.dart'; import '../calendar_providers/cloud_calendar_client.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'microsoft_calendar_errors.dart'; import 'microsoft_calendar_mapper.dart'; import 'microsoft_calendar_models.dart'; @@ -31,7 +31,7 @@ class MicrosoftCalendarApiClient implements CloudCalendarClient { final Future Function()? _unauthorizedRefreshProvider; @override - BusyProvider get provider => TaskProvider.microsoft; + BusyProvider get provider => BusyProvider.microsoft; @override CalendarProviderCapabilities get capabilities => diff --git a/lib/src/microsoft_calendar/microsoft_calendar_mapper.dart b/lib/src/microsoft_calendar/microsoft_calendar_mapper.dart index 4193b93..142ed29 100644 --- a/lib/src/microsoft_calendar/microsoft_calendar_mapper.dart +++ b/lib/src/microsoft_calendar/microsoft_calendar_mapper.dart @@ -2,12 +2,12 @@ import '../calendar_providers/calendar_colors.dart'; import '../calendar_providers/calendar_mutation.dart'; import '../calendar_providers/calendar_description.dart'; import '../calendar_providers/calendar_sync_dto.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; CalendarSourceDto microsoftCalendarSourceFromJson(Map json) { final canEdit = json['canEdit']; return CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: json['id']?.toString() ?? '', summary: json['name']?.toString().trim().isNotEmpty == true ? json['name']!.toString() @@ -17,7 +17,7 @@ CalendarSourceDto microsoftCalendarSourceFromJson(Map json) { hidden: false, readOnly: canEdit is bool ? !canEdit : false, backgroundColor: calendarSourceBackgroundColorHex( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, backgroundColor: json['hexColor']?.toString(), colorId: json['color']?.toString(), ), @@ -39,7 +39,7 @@ CalendarEventDto microsoftCalendarEventFromJson( final isAllDay = json['isAllDay'] == true; final status = json['isCancelled'] == true ? 'cancelled' : null; return CalendarEventDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: calendarId, providerEventId: json['id']?.toString() ?? '', providerRecurringEventId: json['seriesMasterId']?.toString(), diff --git a/lib/src/microsoft_todo/api/microsoft_todo_api_client.dart b/lib/src/microsoft_todo/api/microsoft_todo_api_client.dart index 170106d..40f150d 100644 --- a/lib/src/microsoft_todo/api/microsoft_todo_api_client.dart +++ b/lib/src/microsoft_todo/api/microsoft_todo_api_client.dart @@ -47,7 +47,32 @@ abstract interface class MicrosoftTodoApiClient { Future deleteTask({required String taskListId, required String taskId}); } -class MicrosoftTodoRestApiClient implements MicrosoftTodoApiClient { +abstract interface class MicrosoftTodoChecklistApiClient { + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? nextLink, + }); + Future createChecklistItem({ + required String taskListId, + required String taskId, + required Map body, + }); + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + required Map patch, + }); + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }); +} + +class MicrosoftTodoRestApiClient + implements MicrosoftTodoApiClient, MicrosoftTodoChecklistApiClient { MicrosoftTodoRestApiClient({ required http.Client httpClient, required Uri baseUri, @@ -198,6 +223,60 @@ class MicrosoftTodoRestApiClient implements MicrosoftTodoApiClient { return _requestEmpty('DELETE', _uri(microsoftTaskPath(taskListId, taskId))); } + @override + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? nextLink, + }) async { + final json = await _requestJson( + 'GET', + _uriOrFullUrl(nextLink, microsoftChecklistItemsPath(taskListId, taskId)), + ); + return MicrosoftTodoChecklistItemsPageDto.fromJson(json); + } + + @override + Future createChecklistItem({ + required String taskListId, + required String taskId, + required Map body, + }) async { + final json = await _requestJson( + 'POST', + _uri(microsoftChecklistItemsPath(taskListId, taskId)), + body: body, + ); + return MicrosoftTodoChecklistItemDto.fromJson(json); + } + + @override + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + required Map patch, + }) async { + final json = await _requestJson( + 'PATCH', + _uri(microsoftChecklistItemPath(taskListId, taskId, checklistItemId)), + body: patch, + ); + return MicrosoftTodoChecklistItemDto.fromJson(json); + } + + @override + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }) { + return _requestEmpty( + 'DELETE', + _uri(microsoftChecklistItemPath(taskListId, taskId, checklistItemId)), + ); + } + Future _requestEmpty(String method, Uri uri) async { final response = await _send(method, uri); if (response.statusCode < 200 || response.statusCode >= 300) { diff --git a/lib/src/microsoft_todo/api/microsoft_todo_api_models.dart b/lib/src/microsoft_todo/api/microsoft_todo_api_models.dart index eb9f9d7..59d0e7c 100644 --- a/lib/src/microsoft_todo/api/microsoft_todo_api_models.dart +++ b/lib/src/microsoft_todo/api/microsoft_todo_api_models.dart @@ -180,6 +180,59 @@ class MicrosoftTodoTaskDto { final String? removedReason; } +class MicrosoftTodoChecklistItemDto { + const MicrosoftTodoChecklistItemDto({ + required this.id, + required this.rawJson, + this.displayName, + this.isChecked, + this.createdDateTime, + this.checkedDateTime, + }); + + factory MicrosoftTodoChecklistItemDto.fromJson(Map json) { + return MicrosoftTodoChecklistItemDto( + id: json['id']?.toString() ?? '', + displayName: microsoftStringOrNull(json['displayName']), + isChecked: microsoftBoolOrNull(json['isChecked']), + createdDateTime: microsoftStringOrNull(json['createdDateTime']), + checkedDateTime: microsoftStringOrNull(json['checkedDateTime']), + rawJson: Map.unmodifiable(json), + ); + } + + final String id; + final String? displayName; + final bool? isChecked; + final String? createdDateTime; + final String? checkedDateTime; + final Map rawJson; +} + +class MicrosoftTodoChecklistItemsPageDto { + const MicrosoftTodoChecklistItemsPageDto({ + required this.items, + required this.rawJson, + this.nextLink, + }); + + factory MicrosoftTodoChecklistItemsPageDto.fromJson( + Map json, + ) { + return MicrosoftTodoChecklistItemsPageDto( + items: microsoftJsonObjectList( + json['value'], + ).map(MicrosoftTodoChecklistItemDto.fromJson).toList(), + nextLink: microsoftStringOrNull(json['@odata.nextLink']), + rawJson: Map.unmodifiable(json), + ); + } + + final List items; + final String? nextLink; + final Map rawJson; +} + class MicrosoftTodoTaskListsPageDto { const MicrosoftTodoTaskListsPageDto({ required this.items, diff --git a/lib/src/microsoft_todo/api/microsoft_todo_paths.dart b/lib/src/microsoft_todo/api/microsoft_todo_paths.dart index 7eead65..684712a 100644 --- a/lib/src/microsoft_todo/api/microsoft_todo_paths.dart +++ b/lib/src/microsoft_todo/api/microsoft_todo_paths.dart @@ -19,3 +19,16 @@ String microsoftTasksDeltaPath(String taskListId) { String microsoftTaskPath(String taskListId, String taskId) { return '${microsoftTasksPath(taskListId)}/${Uri.encodeComponent(taskId)}'; } + +String microsoftChecklistItemsPath(String taskListId, String taskId) { + return '${microsoftTaskPath(taskListId, taskId)}/checklistItems'; +} + +String microsoftChecklistItemPath( + String taskListId, + String taskId, + String checklistItemId, +) { + return '${microsoftChecklistItemsPath(taskListId, taskId)}/' + '${Uri.encodeComponent(checklistItemId)}'; +} diff --git a/lib/src/microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart b/lib/src/microsoft_todo/api/microsoft_todo_task_remote_client.dart similarity index 76% rename from lib/src/microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart rename to lib/src/microsoft_todo/api/microsoft_todo_task_remote_client.dart index a06c918..676d553 100644 --- a/lib/src/microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart +++ b/lib/src/microsoft_todo/api/microsoft_todo_task_remote_client.dart @@ -1,12 +1,13 @@ -import '../../google_tasks/api/google_tasks_api_client.dart'; -import '../../google_tasks/api/google_tasks_api_error.dart'; -import '../../google_tasks/api/google_tasks_api_models.dart'; +import '../../features/tasks/domain/task_remote_client.dart'; +import '../../features/tasks/domain/task_remote_error.dart'; +import '../../features/tasks/domain/task_remote_models.dart'; import 'microsoft_todo_api_client.dart'; import 'microsoft_todo_api_error.dart'; import 'microsoft_todo_api_models.dart'; -class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { - MicrosoftTodoGoogleTasksAdapter({ +class MicrosoftTodoTaskRemoteClient + implements TaskRemoteClient, TaskChecklistRemoteClient { + MicrosoftTodoTaskRemoteClient({ required MicrosoftTodoApiClient client, required String defaultTimeZone, DateTime Function()? nowUtc, @@ -46,7 +47,7 @@ class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { nextLink = page.nextLink; } while (nextLink != null && nextLink.isNotEmpty); - throw const GoogleTasksApiError( + throw const TaskRemoteError( statusCode: 404, code: 'not_found', message: 'Microsoft To Do task list was not found.', @@ -100,13 +101,87 @@ class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { @override Future clearCompletedTasks(String taskListId) { - throw const GoogleTasksApiError( + throw const TaskRemoteError( statusCode: 400, code: 'unsupported_provider_operation', message: 'Clear completed is not supported for Microsoft To Do accounts.', ); } + @override + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? pageToken, + }) async { + final page = await _translateMicrosoftErrors( + () => _checklistClient.listChecklistItemsPage( + taskListId: taskListId, + taskId: taskId, + nextLink: pageToken, + ), + ); + return TaskChecklistItemsPageDto( + items: page.items.map(_checklistItemDto).toList(), + nextPageToken: page.nextLink, + rawJson: page.rawJson, + ); + } + + @override + Future createChecklistItem({ + required String taskListId, + required String taskId, + required String title, + bool completed = false, + }) async { + final item = await _translateMicrosoftErrors( + () => _checklistClient.createChecklistItem( + taskListId: taskListId, + taskId: taskId, + body: {'displayName': title, 'isChecked': completed}, + ), + ); + return _checklistItemDto(item); + } + + @override + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + String? title, + bool? completed, + }) async { + final item = await _translateMicrosoftErrors( + () => _checklistClient.updateChecklistItem( + taskListId: taskListId, + taskId: taskId, + checklistItemId: checklistItemId, + patch: { + if (title != null) 'displayName': title, + if (completed != null) 'isChecked': completed, + }, + ), + ); + return _checklistItemDto(item); + } + + @override + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }) { + return _translateMicrosoftErrors( + () => _checklistClient.deleteChecklistItem( + taskListId: taskListId, + taskId: taskId, + checklistItemId: checklistItemId, + ), + ); + } + @override Future createTask({ required String taskListId, @@ -115,7 +190,7 @@ class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { required TaskCreate create, }) async { if (parentTaskId != null || previousSiblingTaskId != null) { - throw const GoogleTasksApiError( + throw const TaskRemoteError( statusCode: 400, code: 'unsupported_provider_operation', message: @@ -185,7 +260,7 @@ class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { String? previousSiblingTaskId, String? destinationTaskListId, }) { - throw const GoogleTasksApiError( + throw const TaskRemoteError( statusCode: 400, code: 'unsupported_provider_operation', message: @@ -247,15 +322,28 @@ class MicrosoftTodoGoogleTasksAdapter implements GoogleTasksApiClient { try { return await request(); } on MicrosoftTodoApiError catch (error) { - throw GoogleTasksApiError( + throw TaskRemoteError( statusCode: error.statusCode, - code: error.code, + code: error.code ?? 'microsoft_todo_error', message: error.message, - rawJson: error.rawJson, + providerDetails: error.rawJson, + retryable: error.statusCode == 429 || error.statusCode >= 500, ); } } + MicrosoftTodoChecklistApiClient get _checklistClient { + final client = _client; + if (client is MicrosoftTodoChecklistApiClient) { + return client as MicrosoftTodoChecklistApiClient; + } + throw const TaskRemoteError( + statusCode: 400, + code: 'unsupported_provider_operation', + message: 'Microsoft To Do checklist operations are unavailable.', + ); + } + Map _microsoftTaskPatch(Map fields) { final patch = {}; if (fields.containsKey('title')) { @@ -368,6 +456,17 @@ TaskDto _taskDto(MicrosoftTodoTaskDto dto) { ); } +TaskChecklistItemDto _checklistItemDto(MicrosoftTodoChecklistItemDto dto) { + return TaskChecklistItemDto( + id: dto.id, + title: dto.displayName ?? '', + completed: dto.isChecked ?? false, + createdAtUtc: _parseUtc(dto.createdDateTime), + completedAtUtc: _parseUtc(dto.checkedDateTime), + rawJson: dto.rawJson, + ); +} + DateTime? _parseUtc(String? value) { if (value == null || value.isEmpty) { return null; diff --git a/lib/src/microsoft_todo/oauth/microsoft_oauth_service.dart b/lib/src/microsoft_todo/oauth/microsoft_oauth_service.dart index a74a08c..8f2ab0c 100644 --- a/lib/src/microsoft_todo/oauth/microsoft_oauth_service.dart +++ b/lib/src/microsoft_todo/oauth/microsoft_oauth_service.dart @@ -6,8 +6,9 @@ import 'package:logging/logging.dart'; import '../../config/build_config.dart'; import '../../core/logging/redacting_logger.dart'; import '../../google_tasks/oauth/oauth_loopback_flow.dart'; -import '../../google_tasks/oauth/oauth_models.dart'; -import '../../google_tasks/oauth/oauth_token_store.dart'; +import '../../providers/busy_provider.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import '../api/microsoft_todo_api_client.dart'; import '../api/microsoft_todo_api_models.dart'; @@ -26,7 +27,7 @@ class MicrosoftOAuthService { MicrosoftOAuthService({ required BuildConfig config, required http.Client httpClient, - required OAuthTokenStore tokenStore, + required SecretStore tokenStore, required OAuthLoopbackFlow loopbackFlow, DateTime Function()? nowUtc, }) : _config = config, @@ -37,7 +38,7 @@ class MicrosoftOAuthService { final BuildConfig _config; final http.Client _httpClient; - final OAuthTokenStore _tokenStore; + final SecretStore _tokenStore; final OAuthLoopbackFlow _loopbackFlow; final DateTime Function() _nowUtc; final RedactingLogger _logger = RedactingLogger( @@ -84,7 +85,11 @@ class MicrosoftOAuthService { } final accountId = 'microsoft:${user.id}'; - await _tokenStore.saveTokenSet(accountId, tokenSet); + await _tokenStore.saveOAuthTokenSet( + accountId, + BusyProvider.microsoft, + tokenSet, + ); await _tokenStore.setActiveAccountId(accountId); return MicrosoftOAuthSignInResult( accountId: accountId, @@ -96,11 +101,11 @@ class MicrosoftOAuthService { Future cancelSignIn() => _loopbackFlow.cancel(); Future readTokenSet(String accountId) { - return _tokenStore.readTokenSet(accountId); + return _readTokenSet(accountId); } Future validTokenForAccount(String accountId) async { - final tokenSet = await _tokenStore.readTokenSet(accountId); + final tokenSet = await _readTokenSet(accountId); if (tokenSet == null) { throw const OAuthException( 'MicrosoftOAuthMissingToken', @@ -169,7 +174,7 @@ class MicrosoftOAuthService { } Future refreshTokenForAccount(String accountId) async { - final current = await _tokenStore.readTokenSet(accountId); + final current = await _readTokenSet(accountId); if (current == null || !current.canRefresh) { throw const OAuthException( 'MicrosoftOAuthRefreshFailed', @@ -178,7 +183,11 @@ class MicrosoftOAuthService { } final refreshed = await refreshToken(current); - await _tokenStore.saveTokenSet(accountId, refreshed); + await _tokenStore.saveOAuthTokenSet( + accountId, + BusyProvider.microsoft, + refreshed, + ); return refreshed; } @@ -226,12 +235,20 @@ class MicrosoftOAuthService { } Future signOutAccount(String accountId) async { - await _tokenStore.clearTokenSet(accountId); + await _tokenStore.deleteCredential(accountId); if (await _tokenStore.readActiveAccountId() == accountId) { await _tokenStore.clearActiveAccount(); } } + Future _readTokenSet(String accountId) async { + await _tokenStore.migrateLegacyOAuthCredential( + accountId, + BusyProvider.microsoft, + ); + return _tokenStore.readOAuthTokenSet(accountId, BusyProvider.microsoft); + } + Future _getMe(OAuthTokenSet tokenSet) { final client = MicrosoftTodoRestApiClient( httpClient: _httpClient, diff --git a/lib/src/providers/account_authority.dart b/lib/src/providers/account_authority.dart new file mode 100644 index 0000000..640833a --- /dev/null +++ b/lib/src/providers/account_authority.dart @@ -0,0 +1,105 @@ +import 'busy_provider.dart'; + +const googleAccountAuthority = 'https://accounts.google.com'; +const appleICloudAccountAuthority = 'https://caldav.icloud.com'; +const microsoftAuthorityOrigin = 'https://login.microsoftonline.com'; + +final class InvalidAccountAuthorityException implements FormatException { + const InvalidAccountAuthorityException(this.reason, [this.source]); + + final String reason; + + @override + final String? source; + + @override + String get message => reason; + + @override + int? get offset => null; +} + +String normalizeProviderAccountId(BusyProvider provider, String value) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + throw const InvalidAccountAuthorityException( + 'The provider account identifier must not be empty.', + ); + } + return switch (provider) { + BusyProvider.appleICloud => trimmed.toLowerCase(), + BusyProvider.google || + BusyProvider.microsoft || + BusyProvider.nextcloud => trimmed, + }; +} + +String normalizeAccountAuthority( + BusyProvider provider, { + String? authority, + String? tenantId, +}) { + return switch (provider) { + BusyProvider.google => googleAccountAuthority, + BusyProvider.microsoft => _microsoftAuthority(tenantId ?? authority), + BusyProvider.appleICloud => appleICloudAccountAuthority, + BusyProvider.nextcloud => normalizeNextcloudServerAuthority( + authority ?? + (throw const InvalidAccountAuthorityException( + 'Nextcloud requires the canonical server returned by Login Flow v2.', + )), + ), + }; +} + +String normalizeNextcloudServerAuthority(String value) { + final trimmed = value.trim(); + final parsed = Uri.tryParse(trimmed); + if (parsed == null || + parsed.scheme.toLowerCase() != 'https' || + parsed.host.isEmpty || + parsed.userInfo.isNotEmpty || + parsed.hasQuery || + parsed.hasFragment) { + throw InvalidAccountAuthorityException( + 'Nextcloud must return an HTTPS server URL without user information, query, or fragment.', + value, + ); + } + + var path = parsed.path; + while (path.length > 1 && path.endsWith('/')) { + path = path.substring(0, path.length - 1); + } + return Uri( + scheme: 'https', + host: parsed.host.toLowerCase(), + port: parsed.hasPort && parsed.port != 443 ? parsed.port : null, + path: path == '/' ? '' : path, + ).toString(); +} + +String _microsoftAuthority(String? tenantIdOrAuthority) { + final value = tenantIdOrAuthority?.trim(); + if (value == null || value.isEmpty) { + return '$microsoftAuthorityOrigin/common'; + } + final uri = Uri.tryParse(value); + if (uri != null && uri.hasScheme) { + if (uri.scheme.toLowerCase() != 'https' || + uri.host.toLowerCase() != 'login.microsoftonline.com' || + uri.userInfo.isNotEmpty || + uri.hasQuery || + uri.hasFragment) { + throw InvalidAccountAuthorityException( + 'Microsoft authority must use the Microsoft login origin.', + value, + ); + } + final tenant = uri.pathSegments + .where((part) => part.isNotEmpty) + .firstOrNull; + return '$microsoftAuthorityOrigin/${(tenant ?? 'common').toLowerCase()}'; + } + return '$microsoftAuthorityOrigin/${value.toLowerCase()}'; +} diff --git a/lib/src/providers/busy_provider.dart b/lib/src/providers/busy_provider.dart new file mode 100644 index 0000000..2c9b0f5 --- /dev/null +++ b/lib/src/providers/busy_provider.dart @@ -0,0 +1,84 @@ +/// Stable account provider identities persisted by BusyMax. +enum BusyProvider { google, microsoft, appleICloud, nextcloud } + +extension BusyProviderValue on BusyProvider { + String get storageValue => switch (this) { + BusyProvider.google => 'google', + BusyProvider.microsoft => 'microsoft', + BusyProvider.appleICloud => 'apple_icloud', + BusyProvider.nextcloud => 'nextcloud', + }; + + String get displayName => switch (this) { + BusyProvider.google => 'Google', + BusyProvider.microsoft => 'Microsoft', + BusyProvider.appleICloud => 'Apple iCloud', + BusyProvider.nextcloud => 'Nextcloud', + }; +} + +sealed class BusyProviderParseResult { + const BusyProviderParseResult(); + + BusyProvider? get provider; +} + +final class SupportedBusyProvider extends BusyProviderParseResult { + const SupportedBusyProvider(this.value); + + final BusyProvider value; + + @override + BusyProvider get provider => value; +} + +final class UnsupportedStoredProvider extends BusyProviderParseResult { + const UnsupportedStoredProvider(this.storageValue); + + final String? storageValue; + + @override + BusyProvider? get provider => null; +} + +/// A typed corrupt-storage failure. Unknown values are never mapped to a +/// different provider because doing so could send data or credentials to the +/// wrong service. +final class UnsupportedStoredProviderException implements FormatException { + const UnsupportedStoredProviderException(this.storageValue); + + final String? storageValue; + + @override + String get message => 'Unsupported provider value in BusyMax storage.'; + + @override + int? get offset => null; + + @override + String? get source => storageValue; + + @override + String toString() => + 'UnsupportedStoredProviderException(storageValue: $storageValue)'; +} + +abstract final class BusyProviderCodec { + static BusyProviderParseResult parseStorageValue(String? value) { + return switch (value) { + 'google' => const SupportedBusyProvider(BusyProvider.google), + 'microsoft' => const SupportedBusyProvider(BusyProvider.microsoft), + 'apple_icloud' => const SupportedBusyProvider(BusyProvider.appleICloud), + 'nextcloud' => const SupportedBusyProvider(BusyProvider.nextcloud), + _ => UnsupportedStoredProvider(value), + }; + } + + static BusyProvider requireStorageValue(String? value) { + return switch (parseStorageValue(value)) { + SupportedBusyProvider(:final value) => value, + UnsupportedStoredProvider(:final storageValue) => + throw UnsupportedStoredProviderException(storageValue), + }; + } +} diff --git a/lib/src/providers/provider_capabilities.dart b/lib/src/providers/provider_capabilities.dart new file mode 100644 index 0000000..b8a0668 --- /dev/null +++ b/lib/src/providers/provider_capabilities.dart @@ -0,0 +1,131 @@ +import 'busy_provider.dart'; + +enum ProviderAuthenticationMethod { + oauth, + appleAppSpecificPassword, + nextcloudLoginFlowV2, +} + +enum ProviderServiceType { calendar, tasks } + +class ProviderProfileCapabilities { + const ProviderProfileCapabilities({ + required this.provider, + required this.authenticationMethod, + required this.expectedServices, + this.allowsGenericServer = false, + this.allowsInsecureHttp = false, + this.allowsCalendarCollectionMutations = false, + this.allowsTaskCollectionMutations = false, + this.allowsSchedulingMutations = false, + }); + + final BusyProvider provider; + final ProviderAuthenticationMethod authenticationMethod; + final Set expectedServices; + final bool allowsGenericServer; + final bool allowsInsecureHttp; + final bool allowsCalendarCollectionMutations; + final bool allowsTaskCollectionMutations; + final bool allowsSchedulingMutations; +} + +const providerProfiles = { + BusyProvider.google: ProviderProfileCapabilities( + provider: BusyProvider.google, + authenticationMethod: ProviderAuthenticationMethod.oauth, + expectedServices: {ProviderServiceType.calendar, ProviderServiceType.tasks}, + allowsCalendarCollectionMutations: true, + allowsTaskCollectionMutations: true, + ), + BusyProvider.microsoft: ProviderProfileCapabilities( + provider: BusyProvider.microsoft, + authenticationMethod: ProviderAuthenticationMethod.oauth, + expectedServices: {ProviderServiceType.calendar, ProviderServiceType.tasks}, + allowsCalendarCollectionMutations: true, + allowsTaskCollectionMutations: true, + ), + BusyProvider.appleICloud: ProviderProfileCapabilities( + provider: BusyProvider.appleICloud, + authenticationMethod: ProviderAuthenticationMethod.appleAppSpecificPassword, + expectedServices: {ProviderServiceType.calendar}, + ), + BusyProvider.nextcloud: ProviderProfileCapabilities( + provider: BusyProvider.nextcloud, + authenticationMethod: ProviderAuthenticationMethod.nextcloudLoginFlowV2, + expectedServices: {ProviderServiceType.calendar, ProviderServiceType.tasks}, + allowsTaskCollectionMutations: true, + ), +}; + +class AccountServiceCapabilities { + const AccountServiceCapabilities({ + this.hasPrincipal = false, + this.hasCalendarHome = false, + this.hasSchedulingInbox = false, + this.hasSchedulingOutbox = false, + this.supportedReports = const {}, + this.serverFeatures = const {}, + }); + + final bool hasPrincipal; + final bool hasCalendarHome; + final bool hasSchedulingInbox; + final bool hasSchedulingOutbox; + final Set supportedReports; + final Set serverFeatures; +} + +class CollectionCapabilities { + const CollectionCapabilities({ + this.canRead = false, + this.canReadPrivileges = false, + this.canWriteContent = false, + this.canWriteProperties = false, + this.canAddMembers = false, + this.canDeleteMembers = false, + this.canReadFreeBusy = false, + this.supportsEvents = false, + this.supportsTasks = false, + this.supportsSyncCollection = false, + this.supportsCalendarMultiget = false, + this.supportsCalendarQuery = false, + this.supportedCalendarData = const {}, + this.maximumResourceSize, + this.maximumInstances, + this.providerAllowsCollectionMutation = false, + this.providerAllowsSchedulingMutation = false, + }); + + final bool canRead; + final bool canReadPrivileges; + final bool canWriteContent; + final bool canWriteProperties; + final bool canAddMembers; + final bool canDeleteMembers; + final bool canReadFreeBusy; + final bool supportsEvents; + final bool supportsTasks; + final bool supportsSyncCollection; + final bool supportsCalendarMultiget; + final bool supportsCalendarQuery; + final Set supportedCalendarData; + final int? maximumResourceSize; + final int? maximumInstances; + final bool providerAllowsCollectionMutation; + final bool providerAllowsSchedulingMutation; + + bool get isReadOnly => !canWriteContent; + bool get canCreateEvent => supportsEvents && canWriteContent && canAddMembers; + bool get canUpdateEvent => supportsEvents && canWriteContent; + bool get canDeleteEvent => + supportsEvents && canWriteContent && canDeleteMembers; + bool get canCreateTask => supportsTasks && canWriteContent && canAddMembers; + bool get canUpdateTask => supportsTasks && canWriteContent; + bool get canDeleteTask => + supportsTasks && canWriteContent && canDeleteMembers; + bool get canMutateCollection => + providerAllowsCollectionMutation && canWriteProperties; + bool get canSchedule => + providerAllowsSchedulingMutation && canWriteContent && canReadFreeBusy; +} diff --git a/lib/src/schedule/schedule_item.dart b/lib/src/schedule/schedule_item.dart index ca39923..fe6ae85 100644 --- a/lib/src/schedule/schedule_item.dart +++ b/lib/src/schedule/schedule_item.dart @@ -1,4 +1,5 @@ -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import '../features/tasks/domain/task_checklist_item.dart'; enum ScheduleItemKind { calendarEvent, task, localReminder } @@ -132,6 +133,11 @@ class TaskScheduleItem implements ScheduleItem { this.notes, this.categories = const [], this.reminder, + this.parentId, + this.parentTitle, + this.hierarchyDepth = 0, + this.hasSubtasks = false, + this.checklistItems = const [], this.sourceName, this.accountDisplayName, this.accountEmail, @@ -159,6 +165,11 @@ class TaskScheduleItem implements ScheduleItem { @override final List categories; final DateTime? reminder; + final String? parentId; + final String? parentTitle; + final int hierarchyDepth; + final bool hasSubtasks; + final List checklistItems; @override final String? sourceName; @override diff --git a/lib/src/schedule/schedule_projection.dart b/lib/src/schedule/schedule_projection.dart index 6837b00..21f3e9d 100644 --- a/lib/src/schedule/schedule_projection.dart +++ b/lib/src/schedule/schedule_projection.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'schedule_item.dart'; import 'schedule_range.dart'; import 'schedule_scope.dart'; @@ -25,7 +25,7 @@ class ScheduleProjection { ScheduleScope.upcoming => List.of(items), }; filtered.sort(compareScheduleItems); - return filtered; + return arrangeHierarchy(filtered); } static List itemsForDay( @@ -35,7 +35,7 @@ class ScheduleProjection { final range = ScheduleRange.day(day); final matches = items.where((item) => intersects(item, range)).toList(); matches.sort(compareScheduleItems); - return matches; + return arrangeHierarchy(matches); } static Map> groupByDay( @@ -50,8 +50,9 @@ class ScheduleProjection { final key = day(start); groups.putIfAbsent(key, () => []).add(item); } - for (final entry in groups.entries) { - entry.value.sort(compareScheduleItems); + for (final key in groups.keys.toList()) { + groups[key]!.sort(compareScheduleItems); + groups[key] = arrangeHierarchy(groups[key]!); } return groups; } @@ -63,6 +64,65 @@ class ScheduleProjection { ) .toList(); result.sort(compareScheduleItems); + return arrangeHierarchy(result); + } + + static List arrangeHierarchy(List items) { + if (items.length < 2) return List.of(items); + final tasks = {}; + for (final item in items) { + if (item is TaskScheduleItem) { + tasks[_taskHierarchyKey(item)] = item; + } + } + if (tasks.isEmpty) return List.of(items); + + final children = >{}; + for (final task in tasks.values) { + final parentId = task.parentId; + if (parentId == null) continue; + final parentKey = _taskHierarchyKeyFor( + accountId: task.accountId, + sourceId: task.sourceId, + taskId: parentId, + ); + if (!tasks.containsKey(parentKey)) continue; + children.putIfAbsent(parentKey, () => []).add(task); + } + + final result = []; + final emitted = {}; + void emitTask(TaskScheduleItem task, Set ancestors) { + final key = _taskHierarchyKey(task); + if (!emitted.add(key)) return; + result.add(task as T); + if (!ancestors.add(key)) return; + for (final child in children[key] ?? const []) { + emitTask(child, ancestors); + } + ancestors.remove(key); + } + + for (final item in items) { + if (item is! TaskScheduleItem) { + result.add(item); + continue; + } + final parentId = item.parentId; + final parentIsPresent = + parentId != null && + tasks.containsKey( + _taskHierarchyKeyFor( + accountId: item.accountId, + sourceId: item.sourceId, + taskId: parentId, + ), + ); + if (!parentIsPresent) emitTask(item, {}); + } + for (final task in tasks.values) { + emitTask(task, {}); + } return result; } @@ -82,8 +142,10 @@ class ScheduleProjection { if (item is TaskScheduleItem) { final listName = _cleanLabel(item.sourceName) ?? 'Tasks'; return switch (item.provider) { - TaskProvider.google => _dedupeProvider('Google Tasks', listName), - TaskProvider.microsoft => _dedupeProvider('Microsoft To Do', listName), + BusyProvider.google => _dedupeProvider('Google Tasks', listName), + BusyProvider.microsoft => _dedupeProvider('Microsoft To Do', listName), + BusyProvider.appleICloud => _dedupeProvider('Apple iCloud', listName), + BusyProvider.nextcloud => _dedupeProvider('Nextcloud Tasks', listName), }; } return 'BusyMax'; @@ -134,6 +196,18 @@ class ScheduleProjection { } } +String _taskHierarchyKey(TaskScheduleItem task) => _taskHierarchyKeyFor( + accountId: task.accountId, + sourceId: task.sourceId, + taskId: task.id, +); + +String _taskHierarchyKeyFor({ + required String accountId, + required String sourceId, + required String taskId, +}) => '$accountId\u0000$sourceId\u0000$taskId'; + Color? _colorFromHex(String? value) { if (value == null) { return null; diff --git a/lib/src/schedule/schedule_repository.dart b/lib/src/schedule/schedule_repository.dart index 82b0ae4..65ad91b 100644 --- a/lib/src/schedule/schedule_repository.dart +++ b/lib/src/schedule/schedule_repository.dart @@ -4,9 +4,12 @@ import 'package:drift/drift.dart'; import '../calendar_providers/calendar_colors.dart'; import '../calendar_providers/calendar_description.dart'; -import '../db/app_database.dart'; import '../core/time/provider_date_time.dart'; -import '../task_providers/task_provider.dart'; +import '../dav/storage/dav_collection_capabilities.dart'; +import '../db/app_database.dart'; +import '../features/accounts/data/accounts_repository.dart'; +import '../features/tasks/domain/task_checklist_item.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'schedule_filters.dart'; import 'schedule_item.dart'; import 'schedule_projection.dart'; @@ -52,6 +55,12 @@ class ScheduleRepository { _database.accounts, _database.accounts.id.equalsExp(_database.tasks.accountId), ), + leftOuterJoin( + _database.davCollections, + _database.davCollections.id.equalsExp( + _database.taskLists.davCollectionId, + ), + ), ]) ..where(_database.tasks.accountId.equals(accountId)) ..where(_database.tasks.taskListId.equals(taskListId)) @@ -68,7 +77,13 @@ class ScheduleRepository { ) ..where(_database.taskLists.pendingDelete.equals(false)) ..where(_database.taskLists.serverMissing.equals(false)) - ..where(_database.accounts.authState.equals('signed_in')); + ..where( + _database.taskLists.davCollectionId.isNull() | + _database.davCollections.tasksSelected.equals(true), + ) + ..where( + _database.accounts.authState.isIn(accountCachedAvailableStates), + ); return query.watchSingleOrNull().map((row) { if (row == null) { return null; @@ -154,13 +169,134 @@ class ScheduleRepository { ); } + /// Adds every available ancestor of the tasks already present in [items]. + /// + /// Agenda task buckets are paginated independently. Without this closure, a + /// no-date child can be loaded while its overdue parent sits beyond the + /// current overdue page, leaving one logical task tree split across sections. + Future> includeTaskAncestors( + List items, { + ScheduleFilters filters = const ScheduleFilters(), + }) async { + final visibleTasks = items.whereType().toList(); + if (!filters.includeTasks || + visibleTasks.isEmpty || + (filters.taskListFilterActive && filters.taskListKeys.isEmpty)) { + return List.of(items); + } + + final context = await _accountContext(filters); + if (context == null) return List.of(items); + + final query = + _database.select(_database.tasks).join([ + leftOuterJoin( + _database.taskLists, + _database.taskLists.accountId.equalsExp( + _database.tasks.accountId, + ) & + _database.taskLists.id.equalsExp(_database.tasks.taskListId), + ), + leftOuterJoin( + _database.davCollections, + _database.davCollections.id.equalsExp( + _database.taskLists.davCollectionId, + ), + ), + ]) + ..where(_database.tasks.accountId.isIn(context.accountIds)) + ..where(_database.tasks.pendingDelete.equals(false)) + ..where(_database.tasks.serverMissing.equals(false)) + ..where( + _database.tasks.deleted.isNull() | + _database.tasks.deleted.equals(false), + ) + ..where( + _database.tasks.hidden.isNull() | + _database.tasks.hidden.equals(false), + ) + ..where( + _database.taskLists.id.isNull() | + _database.taskLists.serverMissing.equals(false), + ) + ..where( + _database.taskLists.davCollectionId.isNull() | + _database.davCollections.tasksSelected.equals(true), + ); + if (filters.taskListFilterActive) { + query.where(_taskListFilter(filters.taskListKeys)); + } + + final rows = await query.get(); + if (rows.isEmpty) return List.of(items); + + final tasks = []; + final rowsById = {}; + for (final row in rows) { + final task = row.readTable(_database.tasks); + tasks.add(task); + rowsById[_taskKey(task.accountId, task.taskListId, task.id)] = row; + } + final hierarchy = _TaskHierarchyContext(tasks); + final tasksById = { + for (final task in tasks) + _taskKey(task.accountId, task.taskListId, task.id): task, + }; + final includedKeys = { + for (final task in visibleTasks) + _taskKey(task.accountId, task.sourceId, task.id), + }; + final ancestorKeys = {}; + + for (final visibleTask in visibleTasks) { + Task? current = + tasksById[_taskKey( + visibleTask.accountId, + visibleTask.sourceId, + visibleTask.id, + )]; + final visiting = {}; + while (current != null) { + final currentKey = _taskKey( + current.accountId, + current.taskListId, + current.id, + ); + if (!visiting.add(currentKey)) break; + final parent = hierarchy.parentOf(current); + if (parent == null || parent.id == current.id) break; + final parentKey = _taskKey( + parent.accountId, + parent.taskListId, + parent.id, + ); + if (includedKeys.add(parentKey)) ancestorKeys.add(parentKey); + current = parent; + } + } + + if (ancestorKeys.isEmpty) return List.of(items); + return [ + ...items, + for (final key in ancestorKeys) + if (rowsById[key] case final row?) + _taskItemFromRow( + row, + context.providers, + context.accountDisplayNames, + context.accountEmails, + hierarchy, + ), + ]; + } + Future> _accountIds(ScheduleFilters filters) async { + final query = _database.select(_database.accounts) + ..where((row) => row.authState.isIn(accountCachedAvailableStates)); if (filters.accountIds.isNotEmpty) { - return filters.accountIds.toList(); + query.where((row) => row.id.isIn(filters.accountIds)); } - final accounts = await (_database.select( - _database.accounts, - )..where((row) => row.authState.equals('signed_in'))).get(); + final accounts = await query.get(); return accounts.map((account) => account.id).toList(); } @@ -178,7 +314,7 @@ class ScheduleRepository { accountIds: accountIds, providers: { for (final account in accounts) - account.id: TaskProviderParsing.fromStorageValue(account.provider), + account.id: BusyProviderCodec.requireStorageValue(account.provider), }, accountDisplayNames: { for (final account in accounts) account.id: account.displayName, @@ -211,7 +347,8 @@ class ScheduleRepository { ), ]) ..where(_database.calendarEvents.accountId.isIn(accountIds)) - ..where(_database.calendarEvents.isDeleted.equals(false)); + ..where(_database.calendarEvents.isDeleted.equals(false)) + ..where(_database.calendarEvents.isCancelled.equals(false)); if (filters.sourceFilterActive) { query.where( _database.calendarEvents.calendarSourceId.isIn(filters.sourceIds), @@ -227,7 +364,7 @@ class ScheduleRepository { final descriptionBody = _eventDescriptionBody(event); final provider = providers[event.accountId] ?? - TaskProviderParsing.fromStorageValue(event.provider); + BusyProviderCodec.requireStorageValue(event.provider); if (!searching && !_intersects(range, start, end)) { continue; } @@ -303,6 +440,12 @@ class ScheduleRepository { ) & _database.taskLists.id.equalsExp(_database.tasks.taskListId), ), + leftOuterJoin( + _database.davCollections, + _database.davCollections.id.equalsExp( + _database.taskLists.davCollectionId, + ), + ), ]) ..where(_database.tasks.accountId.isIn(accountIds)) ..where(_database.tasks.pendingDelete.equals(false)) @@ -318,6 +461,10 @@ class ScheduleRepository { ..where( _database.taskLists.id.isNull() | _database.taskLists.serverMissing.equals(false), + ) + ..where( + _database.taskLists.davCollectionId.isNull() | + _database.davCollections.tasksSelected.equals(true), ); if (filters.taskListFilterActive) { query.where(_taskListFilter(filters.taskListKeys)); @@ -326,10 +473,13 @@ class ScheduleRepository { query.where(_taskIncomplete()); } if (!searching) { - final inRange = _taskScheduledInRange(range); + final inRange = + _taskScheduledInRange(range) | + _database.tasks.davCollectionId.isNotNull(); query.where(filters.showNoDateTasks ? inRange | _taskNoDate() : inRange); } final rows = await query.get(); + final hierarchy = await _taskHierarchy(rows); final items = []; for (final row in rows) { final item = _taskItemFromRow( @@ -337,6 +487,7 @@ class ScheduleRepository { providers, accountDisplayNames, accountEmails, + hierarchy, ); if (!filters.showCompletedTasks && item.completed) { continue; @@ -371,6 +522,11 @@ class ScheduleRepository { } final effectiveLimit = limit < 1 ? 1 : limit; + final includesDav = context.providers.values.any( + (provider) => + provider == BusyProvider.appleICloud || + provider == BusyProvider.nextcloud, + ); final query = _database.select(_database.tasks).join([ leftOuterJoin( @@ -380,6 +536,12 @@ class ScheduleRepository { ) & _database.taskLists.id.equalsExp(_database.tasks.taskListId), ), + leftOuterJoin( + _database.davCollections, + _database.davCollections.id.equalsExp( + _database.taskLists.davCollectionId, + ), + ), ]) ..where(_database.tasks.accountId.isIn(context.accountIds)) ..where(_database.tasks.pendingDelete.equals(false)) @@ -396,8 +558,15 @@ class ScheduleRepository { _database.taskLists.id.isNull() | _database.taskLists.serverMissing.equals(false), ) - ..where(databaseFilter) - ..limit(effectiveLimit + 1); + ..where( + _database.taskLists.davCollectionId.isNull() | + _database.davCollections.tasksSelected.equals(true), + ) + ..where( + includesDav + ? databaseFilter | _database.tasks.davCollectionId.isNotNull() + : databaseFilter, + ); if (filters.taskListFilterActive) { query.where(_taskListFilter(filters.taskListKeys)); } @@ -415,6 +584,7 @@ class ScheduleRepository { ]); final rows = await query.get(); + final hierarchy = await _taskHierarchy(rows); final items = []; for (final row in rows) { final item = _taskItemFromRow( @@ -422,6 +592,7 @@ class ScheduleRepository { context.providers, context.accountDisplayNames, context.accountEmails, + hierarchy, ); if (!filters.showCompletedTasks && item.completed) { continue; @@ -430,16 +601,14 @@ class ScheduleRepository { continue; } items.add(item); - if (items.length > effectiveLimit) { - break; - } } - final visibleItems = items.take(effectiveLimit).toList() - ..sort(compareScheduleItems); + items.sort(compareScheduleItems); + final orderedItems = ScheduleProjection.arrangeHierarchy(items); + final visibleItems = orderedItems.take(effectiveLimit).toList(); return ScheduleTaskBucketPage( items: visibleItems, - hasMore: items.length > effectiveLimit, + hasMore: orderedItems.length > effectiveLimit, ); } @@ -448,11 +617,21 @@ class ScheduleRepository { Map providers, Map accountDisplayNames, Map accountEmails, + _TaskHierarchyContext hierarchy, ) { final task = row.readTable(_database.tasks); - final provider = providers[task.accountId] ?? TaskProvider.google; + final provider = providers[task.accountId]; + if (provider == null) { + throw StateError('Schedule task account provider is unavailable.'); + } final list = row.readTableOrNull(_database.taskLists); + final davCollection = row.readTableOrNull(_database.davCollections); final start = _taskStart(task, provider); + final parent = hierarchy.parentOf(task); + final unresolvedParentId = task.parent ?? task.parentUid; + final checklistItems = decodeTaskChecklistItems( + task.microsoftChecklistItemsJson, + ); return TaskScheduleItem( id: task.id, accountId: task.accountId, @@ -471,9 +650,48 @@ class ScheduleRepository { task.microsoftReminderTimeZone, ) : null, + parentId: parent?.id ?? unresolvedParentId, + parentTitle: parent?.title, + hierarchyDepth: hierarchy.depthOf(task), + hasSubtasks: hierarchy.hasChildren(task) || checklistItems.isNotEmpty, + checklistItems: checklistItems, sourceName: list?.title, accountDisplayName: accountDisplayNames[task.accountId], accountEmail: accountEmails[task.accountId], + capabilities: _taskScheduleCapabilities(davCollection), + ); + } + + Future<_TaskHierarchyContext> _taskHierarchy( + List visibleRows, + ) async { + if (visibleRows.isEmpty) return _TaskHierarchyContext.empty; + final visibleTasks = [ + for (final row in visibleRows) row.readTable(_database.tasks), + ]; + final accountIds = {for (final task in visibleTasks) task.accountId}; + final listKeys = { + for (final task in visibleTasks) + _taskListKey(task.accountId, task.taskListId), + }; + final allRows = + await (_database.select(_database.tasks)..where( + (row) => + row.accountId.isIn(accountIds) & + row.pendingDelete.equals(false) & + row.serverMissing.equals(false) & + (row.deleted.isNull() | row.deleted.equals(false)) & + (row.hidden.isNull() | row.hidden.equals(false)), + )) + .get(); + return _TaskHierarchyContext( + allRows + .where( + (task) => listKeys.contains( + _taskListKey(task.accountId, task.taskListId), + ), + ) + .toList(), ); } @@ -559,6 +777,82 @@ class _ScheduleAccountContext { final Map accountEmails; } +class _TaskHierarchyContext { + _TaskHierarchyContext(List tasks) + : _byId = { + for (final task in tasks) + _taskKey(task.accountId, task.taskListId, task.id): task, + }, + _byUid = { + for (final task in tasks) + if (task.icalUid != null && task.icalUid!.isNotEmpty) + _taskKey(task.accountId, task.taskListId, task.icalUid!): task, + } { + for (final task in tasks) { + final parent = parentOf(task); + if (parent != null && parent.id != task.id) { + _parentsWithChildren.add( + _taskKey(parent.accountId, parent.taskListId, parent.id), + ); + } + } + } + + static final empty = _TaskHierarchyContext(const []); + + final Map _byId; + final Map _byUid; + final Map _parents = {}; + final Map _depths = {}; + final Set _parentsWithChildren = {}; + + Task? parentOf(Task task) { + final key = _taskKey(task.accountId, task.taskListId, task.id); + if (_parents.containsKey(key)) return _parents[key]; + Task? parent; + final parentId = task.parent; + if (parentId != null && parentId.isNotEmpty) { + final lookup = _taskKey(task.accountId, task.taskListId, parentId); + parent = _byId[lookup] ?? _byUid[lookup]; + } + final parentUid = task.parentUid; + if (parent == null && parentUid != null && parentUid.isNotEmpty) { + final lookup = _taskKey(task.accountId, task.taskListId, parentUid); + parent = _byUid[lookup] ?? _byId[lookup]; + } + _parents[key] = parent; + return parent; + } + + int depthOf(Task task) => _resolveDepth(task, {}); + + int _resolveDepth(Task task, Set visiting) { + final key = _taskKey(task.accountId, task.taskListId, task.id); + final cached = _depths[key]; + if (cached != null) return cached; + if (!visiting.add(key)) return 0; + final parent = parentOf(task); + final depth = parent == null + ? (task.parent != null || task.parentUid != null ? 1 : 0) + : parent.id == task.id + ? 0 + : 1 + _resolveDepth(parent, visiting); + visiting.remove(key); + _depths[key] = depth; + return depth; + } + + bool hasChildren(Task task) => _parentsWithChildren.contains( + _taskKey(task.accountId, task.taskListId, task.id), + ); +} + +String _taskListKey(String accountId, String taskListId) => + '$accountId\u0000$taskListId'; + +String _taskKey(String accountId, String taskListId, String taskId) => + '$accountId\u0000$taskListId\u0000$taskId'; + Expression _textBefore(GeneratedColumn value, String upperBound) { return value.isNotNull() & value.isSmallerThanValue(upperBound); } @@ -585,7 +879,7 @@ String _dateKey(DateTime value) { ({String? contentType, String? html}) _eventDescriptionBody( CalendarEvent event, ) { - if (event.provider != TaskProvider.microsoft.storageValue) { + if (event.provider != BusyProvider.microsoft.storageValue) { return (contentType: null, html: null); } final rawJson = event.rawJson; @@ -627,17 +921,26 @@ bool matchesScheduleQuery(ScheduleItem item, String query) { item.description ?? '', ...item.categories, ], - if (item is TaskScheduleItem) ...[item.notes ?? '', ...item.categories], + if (item is TaskScheduleItem) ...[ + item.notes ?? '', + item.parentTitle ?? '', + ...item.categories, + ...item.checklistItems.map((subtask) => subtask.title), + ], ].map((value) => value.toLowerCase()).toList(); return terms.every((term) => fields.any((field) => field.contains(term))); } DateTime? _taskStart(Task task, BusyProvider provider) { - if (provider == TaskProvider.microsoft) { + if (provider == BusyProvider.microsoft) { return _parseDateTime(task.microsoftStartDateTime) ?? _parseDateTime(task.microsoftDueDateTime) ?? _parseDate(task.dueUtc); } + if (_isDavProvider(provider)) { + final native = _davTaskScheduleTemporal(task); + return _parseDavTaskTemporal(native) ?? _parseDateTime(task.dueUtc); + } return _parseDate(task.dueUtc); } @@ -653,9 +956,14 @@ DateTime? _taskEnd(Task task, BusyProvider provider) { } bool _taskAllDay(Task task, BusyProvider provider) { - if (provider == TaskProvider.google) { + if (provider == BusyProvider.google) { return true; } + if (_isDavProvider(provider)) { + final native = _davTaskScheduleTemporal(task); + return native?.kind == 'date' || + (native == null && _isDateOnly(task.dueUtc ?? '')); + } final scheduleDateTimes = [ task.microsoftStartDateTime, task.microsoftDueDateTime, @@ -665,6 +973,80 @@ bool _taskAllDay(Task task, BusyProvider provider) { bool _isDateOnly(String value) => !value.contains('T'); +bool _isDavProvider(BusyProvider provider) => + provider == BusyProvider.appleICloud || provider == BusyProvider.nextcloud; + +_DavTaskTemporal? _davTaskScheduleTemporal(Task task) { + final source = task.providerMetadataJson; + if (source == null || source.isEmpty) return null; + try { + final decoded = jsonDecode(source); + if (decoded is! Map) return null; + for (final key in const ['nativeStart', 'nativeDue']) { + final value = decoded[key]; + if (value is! Map) continue; + final raw = value['raw']; + final kind = value['kind']; + if (raw is String && raw.isNotEmpty && kind is String) { + return _DavTaskTemporal(raw: raw, kind: kind); + } + } + } on FormatException { + return null; + } + return null; +} + +DateTime? _parseDavTaskTemporal(_DavTaskTemporal? temporal) { + if (temporal == null) return null; + final match = RegExp( + r'^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(?:Z)?)?$', + ).firstMatch(temporal.raw); + if (match == null) return null; + final parts = [ + for (var index = 1; index <= 6; index++) + int.tryParse(match.group(index) ?? '') ?? 0, + ]; + final wall = DateTime( + parts[0], + parts[1], + parts[2], + parts[3], + parts[4], + parts[5], + ); + if (temporal.kind != 'utcDateTime') return wall; + return DateTime.utc( + wall.year, + wall.month, + wall.day, + wall.hour, + wall.minute, + wall.second, + ).toLocal(); +} + +ScheduleItemCapabilities _taskScheduleCapabilities(DavCollection? collection) { + if (collection == null) return ScheduleItemCapabilities.editable; + try { + final capabilities = collectionCapabilitiesFromStored(collection); + return ScheduleItemCapabilities( + canEdit: capabilities.canUpdateTask, + canDelete: capabilities.canDeleteTask, + ); + } on Object { + // Corrupt or stale capability state must fail closed in mutation UI. + return ScheduleItemCapabilities.readOnly; + } +} + +final class _DavTaskTemporal { + const _DavTaskTemporal({required this.raw, required this.kind}); + + final String raw; + final String kind; +} + bool _intersects(ScheduleRange range, DateTime? start, DateTime? end) { if (start == null) { return true; @@ -780,11 +1162,11 @@ List _eventReminderMinutes( } final map = decoded.cast(); final minutes = switch (provider) { - TaskProvider.microsoft => + BusyProvider.microsoft => map['isReminderOn'] == true ? [map['reminderMinutesBeforeStart']] : const [], - TaskProvider.google => + BusyProvider.google => map['useDefault'] == true ? _googleDefaultReminderMinutes(source) : switch (map['overrides']) { @@ -795,6 +1177,12 @@ List _eventReminderMinutes( ], _ => const [], }, + BusyProvider.appleICloud || + BusyProvider.nextcloud => switch (map['minutes'] ?? map['overrides']) { + final List values => values, + final int value => [value], + _ => const [], + }, }; return [ for (final value in minutes) diff --git a/lib/src/task_providers/task_provider.dart b/lib/src/task_providers/task_provider.dart deleted file mode 100644 index b61f1e5..0000000 --- a/lib/src/task_providers/task_provider.dart +++ /dev/null @@ -1,97 +0,0 @@ -enum TaskProvider { google, microsoft } - -typedef BusyProvider = TaskProvider; - -extension TaskProviderParsing on TaskProvider { - String get storageValue => switch (this) { - TaskProvider.google => 'google', - TaskProvider.microsoft => 'microsoft', - }; - - String get displayName => switch (this) { - TaskProvider.google => 'Google', - TaskProvider.microsoft => 'Microsoft', - }; - - static TaskProvider fromStorageValue(String? value) { - return switch (value) { - 'microsoft' => TaskProvider.microsoft, - _ => TaskProvider.google, - }; - } -} - -class TaskProviderCapabilities { - const TaskProviderCapabilities({ - required this.supportsDueDate, - required this.supportsDueTime, - required this.supportsStartDateTime, - required this.supportsReminderDateTime, - required this.supportsRecurrence, - required this.supportsImportance, - required this.supportsCategories, - required this.supportsTaskHierarchy, - required this.supportsCrossListMove, - required this.supportsClearCompleted, - required this.supportsHiddenTasks, - required this.supportsAssignedTasks, - required this.supportsListRename, - required this.supportsListDelete, - }); - - final bool supportsDueDate; - final bool supportsDueTime; - final bool supportsStartDateTime; - final bool supportsReminderDateTime; - final bool supportsRecurrence; - final bool supportsImportance; - final bool supportsCategories; - final bool supportsTaskHierarchy; - final bool supportsCrossListMove; - final bool supportsClearCompleted; - final bool supportsHiddenTasks; - final bool supportsAssignedTasks; - final bool supportsListRename; - final bool supportsListDelete; -} - -const googleTaskProviderCapabilities = TaskProviderCapabilities( - supportsDueDate: true, - supportsDueTime: false, - supportsStartDateTime: false, - supportsReminderDateTime: false, - supportsRecurrence: false, - supportsImportance: false, - supportsCategories: false, - supportsTaskHierarchy: true, - supportsCrossListMove: true, - supportsClearCompleted: true, - supportsHiddenTasks: true, - supportsAssignedTasks: true, - supportsListRename: true, - supportsListDelete: true, -); - -const microsoftTaskProviderCapabilities = TaskProviderCapabilities( - supportsDueDate: true, - supportsDueTime: true, - supportsStartDateTime: true, - supportsReminderDateTime: true, - supportsRecurrence: true, - supportsImportance: true, - supportsCategories: true, - supportsTaskHierarchy: false, - supportsCrossListMove: false, - supportsClearCompleted: false, - supportsHiddenTasks: false, - supportsAssignedTasks: false, - supportsListRename: true, - supportsListDelete: true, -); - -TaskProviderCapabilities capabilitiesForProvider(TaskProvider provider) { - return switch (provider) { - TaskProvider.google => googleTaskProviderCapabilities, - TaskProvider.microsoft => microsoftTaskProviderCapabilities, - }; -} diff --git a/linux/io.busystack.busymax.metainfo.xml b/linux/io.busystack.busymax.metainfo.xml index 87ec999..e37e6af 100644 --- a/linux/io.busystack.busymax.metainfo.xml +++ b/linux/io.busystack.busymax.metainfo.xml @@ -58,7 +58,8 @@

BusyMax 是一款面向 Linux 桌面的日历与任务管理工具,可在一个工作区中规划日程、任务、提醒和每日安排。

BusyMax 是一款面向 Linux 桌面的日历与任务管理工具,可在一个工作区中规划日程、任务、提醒和每日安排。

BusyMax 是一款適用於 Linux 桌面的行事曆與待辦事項管理工具,可在同一個工作區中規劃活動、待辦事項、提醒與每日行程。

-

It supports Google Calendar, Google Tasks, Microsoft Calendar, and Microsoft To Do.

+

BusyMax connects directly to Google Calendar and Google Tasks, Microsoft Calendar and Microsoft To Do, Apple iCloud Calendar, and Nextcloud Calendar and Nextcloud Tasks.

+

Apple iCloud Calendar uses an app-specific password. Nextcloud authorization opens in the default browser. Apple Reminders is not supported.

This beta release is intended for early Ubuntu App Center and Snap Store testing.

@@ -83,9 +84,18 @@ BusyMax agenda view with upcoming events and tasks. https://raw.githubusercontent.com/busystack/busymax/main/docs/screenshots/main_window_agenda.png + + Direct Google, Microsoft, Apple iCloud Calendar, and Nextcloud account choices. + https://raw.githubusercontent.com/busystack/busymax/main/docs/screenshots/account_provider_selection.png + + + +

Adds Apple iCloud Calendar and Nextcloud Calendar and Tasks connections.

+
+

Beta maintenance release.

diff --git a/pubspec.lock b/pubspec.lock index 64fa673..5f21ce7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -845,7 +845,7 @@ packages: source: hosted version: "1.5.2" posix: - dependency: transitive + dependency: "direct main" description: name: posix sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" @@ -1273,7 +1273,7 @@ packages: source: path version: "0.0.1" xml: - dependency: transitive + dependency: "direct main" description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" diff --git a/pubspec.yaml b/pubspec.yaml index 3d74a7a..333c90d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: busymax description: BusyMax calendar and task manager. publish_to: 'none' -version: 0.1.4 +version: 0.1.5 environment: sdk: ^3.12.0 @@ -29,6 +29,7 @@ dependencies: package_info_plus: ^10.1.0 path: ^1.9.0 path_provider: ^2.1.0 + posix: ^6.5.0 sqlite3: ^3.3.0 sqlite3_flutter_libs: ^0.6.0 system_theme: ^3.3.0 @@ -37,6 +38,7 @@ dependencies: url_launcher: ^6.3.0 uuid: ^4.5.0 xdg_status_notifier_item: ^0.0.1 + xml: 6.6.1 yaru: ^10.2.0 dev_dependencies: diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index e2d0db3..84f0ba5 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,12 +1,16 @@ name: busymax title: BusyMax -version: "0.1.4" +version: "0.1.5" summary: Calendar and task manager description: | BusyMax is a Linux desktop calendar and task manager. - It supports Google Calendar, Google Tasks, Microsoft Calendar, and - Microsoft To Do. + BusyMax connects directly to Google Calendar and Google Tasks, Microsoft + Calendar and Microsoft To Do, Apple iCloud Calendar, and Nextcloud Calendar + and Nextcloud Tasks. Apple iCloud Calendar requires an app-specific password; + Nextcloud authorization opens in the default browser. + + Apple Reminders is not supported. # Snap Store listing translations are managed outside Snapcraft, via the Snap Store web UI. license: Apache-2.0 @@ -67,6 +71,11 @@ parts: - libsecret-1-0 override-prime: | craftctl default + # path_provider_android brings in jni as a transitive Flutter FFI plugin. + # Flutter stages its Linux JNI bridge even though this Linux runner does + # not register or load it. Do not ship an unused library whose JVM + # dependency is intentionally absent from the desktop snap. + rm -f "$CRAFT_PRIME/lib/libdartjni.so" install -Dm644 "$CRAFT_PROJECT_DIR/linux/io.busystack.busymax.desktop" \ "$CRAFT_PRIME/share/applications/io.busystack.busymax.desktop" install -Dm644 "$CRAFT_PROJECT_DIR/linux/io.busystack.busymax.metainfo.xml" \ diff --git a/test/app/about_dialog_test.dart b/test/app/about_dialog_test.dart index b4aef80..3074e99 100644 --- a/test/app/about_dialog_test.dart +++ b/test/app/about_dialog_test.dart @@ -112,10 +112,19 @@ void main() { final scrollView = find.byType(SingleChildScrollView); final closeButton = find.byType(YaruWindowControl); expect(scrollView, findsOneWidget); + final scrollable = find.descendant( + of: scrollView, + matching: find.byType(Scrollable), + ); + expect(scrollable, findsOneWidget); expect(closeButton.hitTestable(), findsOneWidget); final closePosition = tester.getTopLeft(closeButton); - await tester.drag(scrollView, const Offset(0, -400)); + await tester.scrollUntilVisible( + find.text('Source code'), + 200, + scrollable: scrollable, + ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); @@ -358,8 +367,6 @@ void main() { expect(source, contains('https://busystack.org')); expect(source, contains('https://github.com/busystack/busymax')); expect(source, contains('https://www.apache.org/licenses/LICENSE-2.0')); - expect(source, isNot(contains('/issues'))); - expect(source, isNot(contains('https://github.com/albertgee/busymax'))); }); test( diff --git a/test/app/app_bootstrap_provider_test.dart b/test/app/app_bootstrap_provider_test.dart index feb2926..e6a73ef 100644 --- a/test/app/app_bootstrap_provider_test.dart +++ b/test/app/app_bootstrap_provider_test.dart @@ -8,10 +8,11 @@ import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; import 'package:busymax/src/features/auth/data/auth_repository.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_surface.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_service.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; void main() { test('repositories are not created without an active account', () async { @@ -30,6 +31,10 @@ void main() { expect(container.read(activeAccountProvider), isNull); expect(container.read(taskListsRepositoryProvider), isNull); expect(container.read(tasksRepositoryProvider), isNull); + expect( + container.read(selectedAccountCapabilitiesProvider), + noTaskCollectionCapabilities, + ); }); test('repositories are created after session sign-in', () async { @@ -155,7 +160,7 @@ void main() { }); test( - 'loaded session marks account reconnect required when startup sync has no token', + 'loaded session preserves cached account when startup sync needs reconnect', () async { final database = AppDatabase(NativeDatabase.memory()); await _seedSignedInGoogleAccount(database); @@ -194,7 +199,8 @@ void main() { final state = container.read(authSessionControllerProvider); final account = await database.select(database.accounts).getSingle(); expect(zoneError, isNull); - expect(state.status, AuthSessionStatus.signedOut); + expect(state.status, AuthSessionStatus.signedIn); + expect(state.accountId, 'account-1'); expect(account.authState, accountAuthStateReauthRequired); }, ); @@ -206,7 +212,7 @@ Future _seedSignedInGoogleAccount(AppDatabase database) { nowUtc: () => DateTime.utc(2026, 6, 4), ).upsertSignedInAccount( id: 'account-1', - provider: TaskProvider.google, + provider: BusyProvider.google, grantedScopes: googleBusyMaxOAuthScopes.join(' '), ); } diff --git a/test/app/caldav_release_metadata_test.dart b/test/app/caldav_release_metadata_test.dart new file mode 100644 index 0000000..40c9c4c --- /dev/null +++ b/test/app/caldav_release_metadata_test.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test( + 'package metadata uses matching versions and accurate provider names', + () { + final pubspec = File('pubspec.yaml').readAsStringSync(); + final snap = File('snap/snapcraft.yaml').readAsStringSync(); + final metainfo = File( + 'linux/io.busystack.busymax.metainfo.xml', + ).readAsStringSync(); + + final pubspecVersion = _capture( + pubspec, + RegExp(r'^version:\s*([^\s+]+)', multiLine: true), + ); + final snapVersion = _capture( + snap, + RegExp(r'^version:\s*"([^"]+)"', multiLine: true), + ); + final metainfoVersion = _capture( + metainfo, + RegExp(r' collection.hrefKey).toSet(), + second.collections.map((collection) => collection.hrefKey).toSet(), + ); + expect( + fixture.profile.isTrustedCredentialDestination( + first.service.canonicalServiceUri, + accountAuthority: fixture.authority, + ), + isTrue, + ); + + final eventCollections = first.collections + .where((collection) => collection.eventProjectionEnabled) + .toList(); + expect( + eventCollections.length, + greaterThanOrEqualTo(2), + reason: 'Prepare at least two calendars in the dedicated QA account.', + ); + final writable = eventCollections.firstWhere( + (collection) => collection.capabilities.canCreateEvent, + ); + expect(writable.capabilities.isReadOnly, isFalse); + expect(writable.color, isNotNull); + + if (_enabledFlag('BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_WRITABLE')) { + expect( + eventCollections.any( + (collection) => + collection.capabilities.canCreateEvent && + !_samePrincipal( + collection.ownerHref, + first.service.principalHref, + ), + ), + isTrue, + reason: 'Prepare a writable calendar shared into the QA account.', + ); + } + if (_enabledFlag('BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_READ_ONLY')) { + expect( + eventCollections.any( + (collection) => collection.capabilities.isReadOnly, + ), + isTrue, + reason: 'Prepare a read-only shared/subscribed QA calendar.', + ); + } + + await fixture.verifyEvents(writable); + }, + skip: enabled + ? false + : 'Set $_enabledVariable=1 and the BUSYMAX_ICLOUD_LIVE_* credential ' + 'variables to run the Apple iCloud integration test.', + timeout: const Timeout(Duration(minutes: 5)), + ); + + test( + 'real iCloud onboarding, credential replacement, and local removal are atomic', + () async { + final fixture = _LiveICloudFixture.fromEnvironment(); + addTearDown(fixture.close); + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + final secrets = InMemorySecretStore(); + var nextId = 0; + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedNextcloudLoginFlow(), + idFactory: () => 'icloud-live-${nextId += 1}', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) => fixture.discover( + 'icloud-live-onboarding', + accountId: accountId, + credential: credential, + ), + ); + + final connected = await service.connectAppleICloud( + email: fixture.credential.username, + appSpecificPassword: fixture.credential.password, + ); + expect(await database.select(database.accounts).get(), hasLength(1)); + expect( + await database.select(database.davAccountServices).get(), + hasLength(1), + ); + expect(await secrets.readCredential(connected.accountId), isNotNull); + await database + .into(database.pendingOps) + .insert( + PendingOpsCompanion.insert( + id: 'icloud-live-pending', + accountId: connected.accountId, + provider: const Value('apple_icloud'), + entityType: 'event', + operation: 'dav_update', + operationType: const Value('dav.update'), + requestJson: '{}', + createdAtUtc: '2026-08-09T12:00:00.000Z', + updatedAtUtc: '2026-08-09T12:00:00.000Z', + ), + ); + + await service.replaceAppleAppSpecificPassword( + accountId: connected.accountId, + appSpecificPassword: fixture.credential.password, + ); + expect(await database.select(database.pendingOps).get(), hasLength(1)); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'signed_in', + ); + + final removed = await service.removeAccount(connected.accountId); + expect(removed.remoteRevocationAttempted, isFalse); + expect(await database.select(database.accounts).get(), isEmpty); + expect(await database.select(database.davAccountServices).get(), isEmpty); + expect(await database.select(database.davCollections).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + expect(await secrets.readCredential(connected.accountId), isNull); + expect(await secrets.readActiveAccountId(), isNull); + }, + skip: enabled + ? false + : 'Set $_enabledVariable=1 and the BUSYMAX_ICLOUD_LIVE_* credential ' + 'variables to run the Apple iCloud integration test.', + timeout: const Timeout(Duration(minutes: 5)), + ); +} + +final class _LiveICloudFixture { + _LiveICloudFixture({ + required this.authority, + required this.profile, + required this.credential, + required this.client, + required this.transport, + }); + + factory _LiveICloudFixture.fromEnvironment() { + final username = _requiredEnvironment('BUSYMAX_ICLOUD_LIVE_USERNAME'); + final password = _requiredEnvironment('BUSYMAX_ICLOUD_LIVE_PASSWORD'); + final profile = davProviderProfile(BusyProvider.appleICloud); + final authority = profile.bootstrapUri; + final client = http.Client(); + return _LiveICloudFixture( + authority: authority, + profile: profile, + credential: DavBasicCredential(username: username, password: password), + client: client, + transport: DavHttpTransport( + client: client, + profile: profile, + accountAuthority: authority, + ), + ); + } + + final Uri authority; + final DavProviderProfile profile; + final DavBasicCredential credential; + final http.Client client; + final DavHttpTransport transport; + final Set _objectsToDelete = {}; + + Future discover( + String correlationId, { + String accountId = 'apple-icloud-live', + DavBasicCredential? credential, + }) => DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: authority, + accountId: accountId, + credential: credential ?? this.credential, + ).discover(correlationId: correlationId); + + Future verifyEvents(DavCollectionDiscovery collection) async { + final suffix = DateTime.now().microsecondsSinceEpoch.toString(); + final mutations = _mutationService('icloud-events'); + final created = []; + + final uid = 'busymax-icloud-$suffix@example.invalid'; + var result = await mutations.create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: uid, + initialMemberName: 'busymax-icloud-$suffix.ics', + rawIcs: _complexEvent(uid), + componentType: 'VEVENT', + ), + capabilities: collection.capabilities, + correlationId: 'icloud-live-complex-create', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + var current = result.canonicalObject!; + created.add(current); + _objectsToDelete.add(current.requestUri); + _expectComplexPreservation(current.rawIcsBody!); + + for (final variant in [ + ( + 'all-day', + IcalTemporalKind.date, + 'DTSTART;VALUE=DATE:20260812', + 'DTEND;VALUE=DATE:20260813', + ), + ( + 'floating', + IcalTemporalKind.floatingDateTime, + 'DTSTART:20260813T090000', + 'DTEND:20260813T100000', + ), + ( + 'utc', + IcalTemporalKind.utcDateTime, + 'DTSTART:20260814T160000Z', + 'DTEND:20260814T170000Z', + ), + ]) { + final variantUid = 'busymax-icloud-${variant.$1}-$suffix@example.invalid'; + final variantResult = await mutations.create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: variantUid, + initialMemberName: 'busymax-icloud-${variant.$1}-$suffix.ics', + rawIcs: _simpleEvent( + uid: variantUid, + summary: 'BusyMax iCloud ${variant.$1}', + startLine: variant.$3, + endLine: variant.$4, + ), + componentType: 'VEVENT', + ), + capabilities: collection.capabilities, + correlationId: 'icloud-live-${variant.$1}-create', + ); + expect(variantResult.outcome, DavMutationOutcome.succeeded); + final canonical = variantResult.canonicalObject!; + created.add(canonical); + _objectsToDelete.add(canonical.requestUri); + expect( + IcalSemanticDocument.parse( + canonical.rawIcsBody!, + ).components.single.start!.kind, + variant.$2, + ); + } + + final original = current; + final remoteUpdate = await _mutationClient('icloud-events').conditionalPut( + uri: original.requestUri, + rawIcs: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('LOCATION', 'Apple-side location'), + ], + ).applyTo(original.rawIcsBody!, nowUtc: DateTime.utc(2026, 8, 9, 12)), + correlationId: 'icloud-live-out-of-band-update', + ifMatch: original.etag, + ); + expect(remoteUpdate.status, DavConditionalStatus.success); + + result = await mutations.update( + hrefKey: original.hrefKey, + uri: original.requestUri, + baselineEtag: original.etag!, + baselineRawIcs: original.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'BusyMax iCloud merged title'), + ], + ), + capabilities: collection.capabilities, + correlationId: 'icloud-live-disjoint-merge', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + current = result.canonicalObject!; + expect(current.rawIcsBody, contains('SUMMARY:BusyMax iCloud merged title')); + expect(current.rawIcsBody, contains('LOCATION:Apple-side location')); + _expectComplexPreservation(current.rawIcsBody!); + + final conflictBaseline = current; + final conflictUpdate = await _mutationClient('icloud-events') + .conditionalPut( + uri: current.requestUri, + rawIcs: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'Apple conflicting title'), + ], + ).applyTo(current.rawIcsBody!, nowUtc: DateTime.utc(2026, 8, 9, 12)), + correlationId: 'icloud-live-conflicting-out-of-band-update', + ifMatch: current.etag, + ); + expect(conflictUpdate.status, DavConditionalStatus.success); + result = await mutations.update( + hrefKey: conflictBaseline.hrefKey, + uri: conflictBaseline.requestUri, + baselineEtag: conflictBaseline.etag!, + baselineRawIcs: conflictBaseline.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'BusyMax conflicting title'), + ], + ), + capabilities: collection.capabilities, + correlationId: 'icloud-live-explicit-conflict', + ); + expect(result.outcome, DavMutationOutcome.conflict); + expect( + result.conflictRemoteObject!.rawIcsBody, + contains('SUMMARY:Apple conflicting title'), + ); + created[0] = result.conflictRemoteObject!; + + for (final object in created) { + final deletion = await mutations.delete( + hrefKey: object.hrefKey, + uri: object.requestUri, + baselineEtag: object.etag!, + baselineRawIcs: object.rawIcsBody!, + isEvent: true, + capabilities: collection.capabilities, + correlationId: 'icloud-live-delete', + ); + expect(deletion.outcome, DavMutationOutcome.succeeded); + _objectsToDelete.remove(object.requestUri); + } + } + + DavMutationHttpClient _mutationClient(String id) => DavMutationHttpClient( + transport: transport, + accountId: 'apple-icloud-live', + collectionId: id, + credential: credential, + ); + + DavConditionalMutationService _mutationService(String id) => + DavConditionalMutationService( + remoteClient: _mutationClient(id), + nowUtc: () => DateTime.utc(2026, 8, 9, 12), + ); + + Future close() async { + for (final uri in _objectsToDelete) { + try { + await transport.send( + DavRequest( + method: 'DELETE', + uri: uri, + accountId: 'apple-icloud-live', + correlationId: 'icloud-live-cleanup', + ), + credential: credential, + ); + } on Object { + // Best effort: every fixture UID and member name is unique and safe to + // locate manually in the dedicated QA calendar after a failed run. + } + } + client.close(); + } +} + +NextcloudLoginFlowV2 _unusedNextcloudLoginFlow() => NextcloudLoginFlowV2( + client: MockClient((_) async => http.Response('', 500)), + browserLauncher: (_) async => false, +); + +bool _samePrincipal(String? ownerHref, Uri principalHref) { + if (ownerHref == null || ownerHref.trim().isEmpty) return false; + final owner = Uri.tryParse(ownerHref); + return (owner?.path ?? ownerHref) == principalHref.path; +} + +void _expectComplexPreservation(String rawIcs) { + expect(rawIcs, contains('RRULE:FREQ=WEEKLY;COUNT=4')); + expect(rawIcs, contains('EXDATE;TZID=America/Vancouver:20260816T090000')); + expect(rawIcs, contains('RDATE;TZID=America/Vancouver:20260906T090000')); + expect(rawIcs, contains('RECURRENCE-ID;TZID=America/Vancouver')); + expect(rawIcs, contains('STATUS:CANCELLED')); + expect(rawIcs, contains('CATEGORIES:BusyMax,iCloud')); + expect(rawIcs, contains('X-BUSYMAX-ICLOUD-QA;X-PARAM="alpha,beta":opaque')); + expect('BEGIN:VALARM'.allMatches(rawIcs), hasLength(2)); + expect(rawIcs, contains('ACTION:AUDIO')); +} + +String _complexEvent(String uid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//iCloud Integration Test//EN\r +BEGIN:VTIMEZONE\r +TZID:America/Vancouver\r +BEGIN:STANDARD\r +DTSTART:20251102T020000\r +TZOFFSETFROM:-0700\r +TZOFFSETTO:-0800\r +TZNAME:PST\r +END:STANDARD\r +BEGIN:DAYLIGHT\r +DTSTART:20260308T020000\r +TZOFFSETFROM:-0800\r +TZOFFSETTO:-0700\r +TZNAME:PDT\r +END:DAYLIGHT\r +END:VTIMEZONE\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260809T120000Z\r +DTSTART;TZID=America/Vancouver:20260809T090000\r +DTEND;TZID=America/Vancouver:20260809T100000\r +RRULE:FREQ=WEEKLY;COUNT=4\r +EXDATE;TZID=America/Vancouver:20260816T090000\r +RDATE;TZID=America/Vancouver:20260906T090000\r +SUMMARY:BusyMax iCloud event\r +CATEGORIES:BusyMax,iCloud\r +X-BUSYMAX-ICLOUD-QA;X-PARAM="alpha,beta":opaque\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT15M\r +DESCRIPTION:Visible reminder\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +ATTACH:Glass\r +X-ALARM-QA:opaque\r +END:VALARM\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260809T120000Z\r +RECURRENCE-ID;TZID=America/Vancouver:20260823T090000\r +DTSTART;TZID=America/Vancouver:20260823T110000\r +DTEND;TZID=America/Vancouver:20260823T120000\r +SUMMARY:BusyMax moved exception\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260809T120000Z\r +RECURRENCE-ID;TZID=America/Vancouver:20260830T090000\r +DTSTART;TZID=America/Vancouver:20260830T090000\r +DTEND;TZID=America/Vancouver:20260830T100000\r +STATUS:CANCELLED\r +SUMMARY:BusyMax cancelled exception\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _simpleEvent({ + required String uid, + required String summary, + required String startLine, + required String endLine, +}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//iCloud Integration Test//EN\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260809T120000Z\r +$startLine\r +$endLine\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +bool _enabledFlag(String name) => Platform.environment[name] == '1'; + +String _requiredEnvironment(String name) { + final value = Platform.environment[name]?.trim(); + if (value == null || value.isEmpty) { + throw StateError('$name is required when $_enabledVariable=1.'); + } + return value; +} diff --git a/test/dav/auth/dav_account_dialogs_test.dart b/test/dav/auth/dav_account_dialogs_test.dart new file mode 100644 index 0000000..e20f9d6 --- /dev/null +++ b/test/dav/auth/dav_account_dialogs_test.dart @@ -0,0 +1,51 @@ +import 'package:busymax/src/dav/auth/dav_account_dialogs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../test_localized_app.dart'; + +void main() { + testWidgets('Nextcloud form explains and accepts a copied CalDAV address', ( + tester, + ) async { + late BuildContext hostContext; + await tester.pumpWidget( + localizedTestApp( + child: Builder( + builder: (context) { + hostContext = context; + return const SizedBox.shrink(); + }, + ), + ), + ); + + final result = showNextcloudServerDialog(hostContext); + await tester.pumpAndSettle(); + + final field = tester.widget( + find.byKey(const Key('nextcloud-server-field')), + ); + expect(field.decoration?.labelText, 'Nextcloud server or CalDAV address'); + expect( + field.decoration?.hintText, + 'https://cloud.example.com/remote.php/dav', + ); + expect( + field.decoration?.helperText, + 'Enter your Nextcloud server URL, or paste the primary CalDAV address ' + 'copied from Nextcloud.', + ); + expect(field.decoration?.helperMaxLines, 2); + + const copiedAddress = 'https://cloud.example.test/remote.php/dav'; + await tester.enterText( + find.byKey(const Key('nextcloud-server-field')), + copiedAddress, + ); + await tester.tap(find.text('Connect')); + await tester.pumpAndSettle(); + + expect(await result, copiedAddress); + }); +} diff --git a/test/dav/auth/dav_account_onboarding_service_test.dart b/test/dav/auth/dav_account_onboarding_service_test.dart new file mode 100644 index 0000000..dbe43d3 --- /dev/null +++ b/test/dav/auth/dav_account_onboarding_service_test.dart @@ -0,0 +1,682 @@ +import 'dart:convert'; + +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/auth/dav_account_onboarding_service.dart'; +import 'package:busymax/src/dav/auth/nextcloud_login_flow_v2.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + late AppDatabase database; + late InMemorySecretStore secrets; + + setUp(() { + database = AppDatabase(NativeDatabase.memory()); + secrets = InMemorySecretStore(); + }); + + tearDown(() => database.close()); + + test( + 'Apple discovery completes before trimmed opaque secret is stored', + () async { + var discoveryCalled = false; + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedLoginFlow(), + idFactory: () => 'apple-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async { + discoveryCalled = true; + expect(await secrets.readCredential(accountId), isNull); + expect(provider, BusyProvider.appleICloud); + expect(accountAuthority, Uri.parse('https://caldav.icloud.com')); + expect(credential.username, 'Person@Example.test'); + expect(credential.password, 'abcd-efgh-ijkl-mnop'); + return _discovery(accountId, provider, accountAuthority); + }, + ); + + final result = await service.connectAppleICloud( + email: ' Person@Example.test ', + appSpecificPassword: ' abcd-efgh-ijkl-mnop ', + ); + + expect(discoveryCalled, isTrue); + expect(result.accountId, 'apple_icloud:apple-id'); + final stored = await secrets.readCredential(result.accountId); + expect(stored, isA()); + final apple = stored! as AppleICloudSecretRecord; + expect(apple.username, 'Person@Example.test'); + expect(apple.appSpecificPassword, 'abcd-efgh-ijkl-mnop'); + expect(apple.toString(), isNot(contains(apple.appSpecificPassword))); + final account = await database.select(database.accounts).getSingle(); + expect(account.provider, 'apple_icloud'); + expect(account.providerAccountId, 'person@example.test'); + expect(account.tasksEnabled, isFalse); + expect(account.authState, 'signed_in'); + expect( + await database.select(database.davAccountServices).get(), + hasLength(1), + ); + }, + ); + + test('failed discovery leaves no account or credential', () async { + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedLoginFlow(), + idFactory: () => 'failed-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavAuthRejected', + safeMessage: 'Credential rejected.', + ), + ); + + await expectLater( + service.connectAppleICloud( + email: 'person@example.test', + appSpecificPassword: 'bad-password', + ), + throwsA(isA()), + ); + expect(await database.select(database.accounts).get(), isEmpty); + expect(await secrets.readCredential('apple_icloud:failed-id'), isNull); + }); + + test( + 'Nextcloud stores returned canonical server, login name, and app password', + () async { + var requests = 0; + final flow = NextcloudLoginFlowV2( + client: MockClient((request) async { + requests += 1; + if (requests == 1) { + return http.Response( + jsonEncode({ + 'poll': { + 'token': 'temporary-token', + 'endpoint': + 'https://entered.example.test/cloud/login/v2/poll', + }, + 'login': + 'https://entered.example.test/cloud/login/v2/flow/browser', + }), + 200, + ); + } + return http.Response( + jsonEncode({ + 'server': 'https://canonical.example.test/cloud/', + 'loginName': 'canonical-user', + 'appPassword': 'generated-app-password', + }), + 200, + ); + }), + browserLauncher: (_) async => true, + delay: (_) async {}, + ); + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: flow, + idFactory: () => 'nextcloud-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async { + expect(provider, BusyProvider.nextcloud); + expect( + accountAuthority, + Uri.parse('https://canonical.example.test/cloud'), + ); + expect(credential.username, 'canonical-user'); + expect(credential.password, 'generated-app-password'); + expect(await secrets.readCredential(accountId), isNull); + return _discovery(accountId, provider, accountAuthority); + }, + ); + + final result = await service.connectNextcloud( + enteredServer: 'entered.example.test/cloud', + ); + + final account = await database.select(database.accounts).getSingle(); + expect(account.authority, 'https://canonical.example.test/cloud'); + expect(account.providerAccountId, 'canonical-user'); + expect(account.tasksEnabled, isTrue); + final credential = + await secrets.readCredential(result.accountId) + as NextcloudSecretRecord; + expect( + credential.canonicalServer, + Uri.parse('https://canonical.example.test/cloud'), + ); + expect(credential.loginName, 'canonical-user'); + expect(credential.appPassword, 'generated-app-password'); + expect(credential.toString(), isNot(contains('generated-app-password'))); + }, + ); + + test( + 'Nextcloud reconnect replaces only the verified credential and preserves local state', + () async { + final initialService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(appPassword: 'old-password'), + idFactory: () => 'nextcloud-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => _discovery(accountId, provider, accountAuthority), + ); + final connected = await initialService.connectNextcloud( + enteredServer: 'cloud.example.test', + ); + await _insertNextcloudLocalState(database, connected.accountId); + await _markPendingAuthenticationBlocked(database); + + var verifiedBeforeReplacement = false; + final reconnectService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(appPassword: 'new-password'), + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async { + final stored = + await secrets.readCredential(accountId) + as NextcloudSecretRecord; + expect(stored.appPassword, 'old-password'); + expect(credential.password, 'new-password'); + verifiedBeforeReplacement = true; + return _discovery(accountId, provider, accountAuthority); + }, + ); + + final reconnected = await reconnectService.reconnectNextcloud( + accountId: connected.accountId, + enteredServer: 'cloud.example.test', + ); + + expect(verifiedBeforeReplacement, isTrue); + expect(reconnected.accountId, connected.accountId); + final stored = + await secrets.readCredential(connected.accountId) + as NextcloudSecretRecord; + expect(stored.appPassword, 'new-password'); + expect(await database.select(database.accounts).get(), hasLength(1)); + expect(await database.select(database.taskLists).get(), hasLength(1)); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'pending'); + expect(pending.retryClassification, 'credential_replaced'); + expect(pending.attemptCount, 0); + expect(pending.nextAttemptAtUtc, isNull); + expect(pending.lastErrorCode, isNull); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'signed_in', + ); + }, + ); + + test( + 'Nextcloud reconnect rejects a different identity without touching local state', + () async { + final initialService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(appPassword: 'old-password'), + idFactory: () => 'nextcloud-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => _discovery(accountId, provider, accountAuthority), + ); + final connected = await initialService.connectNextcloud( + enteredServer: 'cloud.example.test', + ); + await _insertNextcloudLocalState(database, connected.accountId); + await _markPendingAuthenticationBlocked(database); + var discoveryCalled = false; + final reconnectService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow( + loginName: 'different-user', + appPassword: 'untrusted-password', + ), + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async { + discoveryCalled = true; + return _discovery(accountId, provider, accountAuthority); + }, + ); + + await expectLater( + reconnectService.reconnectNextcloud( + accountId: connected.accountId, + enteredServer: 'cloud.example.test', + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavNextcloudReconnectIdentityMismatch', + ), + ), + ); + + expect(discoveryCalled, isFalse); + final stored = + await secrets.readCredential(connected.accountId) + as NextcloudSecretRecord; + expect(stored.appPassword, 'old-password'); + expect(await database.select(database.accounts).get(), hasLength(1)); + expect(await database.select(database.taskLists).get(), hasLength(1)); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'auth_blocked'); + expect(pending.nextAttemptAtUtc, startsWith('9999-12-31')); + }, + ); + + test( + 'failed Nextcloud reconnect discovery preserves the old credential and local state', + () async { + final initialService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(appPassword: 'old-password'), + idFactory: () => 'nextcloud-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => _discovery(accountId, provider, accountAuthority), + ); + final connected = await initialService.connectNextcloud( + enteredServer: 'cloud.example.test', + ); + await _insertNextcloudLocalState(database, connected.accountId); + await _markPendingAuthenticationBlocked(database); + final reconnectService = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(appPassword: 'new-password'), + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavAuthRejected', + safeMessage: 'Credential rejected.', + ), + ); + + await expectLater( + reconnectService.reconnectNextcloud( + accountId: connected.accountId, + enteredServer: 'cloud.example.test', + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavAuthRejected', + ), + ), + ); + + final stored = + await secrets.readCredential(connected.accountId) + as NextcloudSecretRecord; + expect(stored.appPassword, 'old-password'); + expect(await database.select(database.accounts).get(), hasLength(1)); + expect(await database.select(database.taskLists).get(), hasLength(1)); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'auth_blocked'); + expect(pending.nextAttemptAtUtc, startsWith('9999-12-31')); + }, + ); + + test('Apple replacement validates before replacing the old secret', () async { + var discoveryCount = 0; + late String accountId; + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedLoginFlow(), + idFactory: () => 'apple-id', + discover: + ({ + required String accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async { + discoveryCount += 1; + if (discoveryCount == 2) { + final stillOld = await secrets.readCredential(accountId); + expect( + (stillOld! as AppleICloudSecretRecord).appSpecificPassword, + 'old-password', + ); + expect(credential.password, 'new-password-with-hyphens'); + } + return _discovery(accountId, provider, accountAuthority); + }, + ); + accountId = (await service.connectAppleICloud( + email: 'person@example.test', + appSpecificPassword: 'old-password', + )).accountId; + await database + .into(database.pendingOps) + .insert( + PendingOpsCompanion.insert( + id: 'apple-auth-blocked', + accountId: accountId, + provider: const Value('apple_icloud'), + entityType: 'event', + operation: 'dav_update', + operationType: const Value('dav.update'), + requestJson: '{}', + state: const Value('auth_blocked'), + retryClassification: const Value('authentication'), + attemptCount: const Value(1), + nextAttemptAtUtc: const Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: const Value('DavCredentialsRevoked'), + createdAtUtc: '2026-08-08T12:00:00.000Z', + updatedAtUtc: '2026-08-08T12:00:00.000Z', + ), + ); + + await service.replaceAppleAppSpecificPassword( + accountId: accountId, + appSpecificPassword: ' new-password-with-hyphens ', + ); + + final replacement = + await secrets.readCredential(accountId) as AppleICloudSecretRecord; + expect(replacement.appSpecificPassword, 'new-password-with-hyphens'); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'pending'); + expect(pending.nextAttemptAtUtc, isNull); + }); + + test( + 'Apple local removal deletes the credential, cache, and pending work', + () async { + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedLoginFlow(), + idFactory: () => 'apple-remove-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => _discovery(accountId, provider, accountAuthority), + ); + final connected = await service.connectAppleICloud( + email: 'person@example.test', + appSpecificPassword: 'app-specific-password', + ); + await database + .into(database.pendingOps) + .insert( + PendingOpsCompanion.insert( + id: 'apple-pending-removal', + accountId: connected.accountId, + provider: const Value('apple_icloud'), + entityType: 'event', + operation: 'dav_create', + operationType: const Value('dav.create'), + requestJson: '{}', + createdAtUtc: '2026-08-08T12:00:00.000Z', + updatedAtUtc: '2026-08-08T12:00:00.000Z', + ), + ); + + final result = await service.removeAccount(connected.accountId); + + expect(result.remoteRevocationAttempted, isFalse); + expect(await secrets.readCredential(connected.accountId), isNull); + expect(await secrets.readActiveAccountId(), isNull); + expect(await database.select(database.accounts).get(), isEmpty); + expect(await database.select(database.davAccountServices).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + }, + ); + + test( + 'remote revocation failure never prevents complete local removal', + () async { + final service = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _successfulLoginFlow(), + idFactory: () => 'nextcloud-id', + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => _discovery(accountId, provider, accountAuthority), + nextcloudCredentialRevoker: + ({required accountId, required credential}) async { + throw const DavException( + kind: DavErrorKind.network, + code: 'RevocationOffline', + safeMessage: 'Offline.', + ); + }, + ); + final connected = await service.connectNextcloud( + enteredServer: 'cloud.example.test', + ); + await database + .into(database.pendingOps) + .insert( + PendingOpsCompanion.insert( + id: 'pending', + accountId: connected.accountId, + provider: const Value('nextcloud'), + entityType: 'event', + operation: 'dav_create', + operationType: const Value('dav.create'), + requestJson: '{}', + createdAtUtc: '2026-08-08T12:00:00.000Z', + updatedAtUtc: '2026-08-08T12:00:00.000Z', + ), + ); + + final result = await service.removeAccount(connected.accountId); + + expect(result.remoteRevocationAttempted, isTrue); + expect(result.remoteRevocationSucceeded, isFalse); + expect(result.remoteFailureCode, 'RevocationOffline'); + expect(await secrets.readCredential(connected.accountId), isNull); + expect(await secrets.readActiveAccountId(), isNull); + expect(await database.select(database.accounts).get(), isEmpty); + expect(await database.select(database.davAccountServices).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + }, + ); +} + +NextcloudLoginFlowV2 _unusedLoginFlow() => NextcloudLoginFlowV2( + client: MockClient((_) async => http.Response('', 500)), + browserLauncher: (_) async => false, +); + +NextcloudLoginFlowV2 _successfulLoginFlow({ + String server = 'https://cloud.example.test/', + String loginName = 'alex', + String appPassword = 'app-password', +}) { + var call = 0; + return NextcloudLoginFlowV2( + client: MockClient((_) async { + call += 1; + return call == 1 + ? http.Response( + jsonEncode({ + 'poll': { + 'token': 'token', + 'endpoint': 'https://cloud.example.test/login/v2/poll', + }, + 'login': 'https://cloud.example.test/login/v2/flow/browser', + }), + 200, + ) + : http.Response( + jsonEncode({ + 'server': server, + 'loginName': loginName, + 'appPassword': appPassword, + }), + 200, + ); + }), + browserLauncher: (_) async => true, + delay: (_) async {}, + ); +} + +Future _insertNextcloudLocalState( + AppDatabase database, + String accountId, +) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: accountId, + id: 'cached-list', + title: 'Cached list', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); + await database + .into(database.pendingOps) + .insert( + PendingOpsCompanion.insert( + id: 'pending-reconnect', + accountId: accountId, + provider: const Value('nextcloud'), + entityType: 'task', + operation: 'dav_update', + operationType: const Value('dav.update'), + requestJson: '{}', + createdAtUtc: now, + updatedAtUtc: now, + ), + ); +} + +Future _markPendingAuthenticationBlocked(AppDatabase database) async { + await database + .update(database.pendingOps) + .write( + const PendingOpsCompanion( + state: Value('auth_blocked'), + retryClassification: Value('authentication'), + attemptCount: Value(1), + nextAttemptAtUtc: Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: Value('DavCredentialsRevoked'), + lastErrorMessage: Value('The DAV credential was rejected.'), + ), + ); +} + +DavDiscoveryResult _discovery( + String accountId, + BusyProvider provider, + Uri authority, +) => DavDiscoveryResult( + accountId: accountId, + provider: provider, + service: DavServiceDiscovery( + canonicalServiceUri: authority, + canonicalOrigin: authority.replace(path: ''), + principalHref: authority.resolve('/principals/user/'), + calendarHomeHref: authority.resolve('/calendars/user/'), + calendarUserAddresses: const [], + scheduleInboxHref: null, + scheduleOutboxHref: null, + capabilities: const AccountServiceCapabilities( + hasPrincipal: true, + hasCalendarHome: true, + ), + discoveredAtUtc: DateTime.utc(2026, 8, 8, 12), + lastValidatedAtUtc: DateTime.utc(2026, 8, 8, 12), + providerProfileVersion: 1, + ), + collections: const [], +); diff --git a/test/dav/auth/nextcloud_app_password_revoker_test.dart b/test/dav/auth/nextcloud_app_password_revoker_test.dart new file mode 100644 index 0000000..4371188 --- /dev/null +++ b/test/dav/auth/nextcloud_app_password_revoker_test.dart @@ -0,0 +1,113 @@ +import 'dart:convert'; + +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/auth/nextcloud_app_password_revoker.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + final credential = NextcloudSecretRecord( + canonicalServer: Uri.parse('https://cloud.example.test/nextcloud'), + loginName: 'alex', + appPassword: 'app-secret', + ); + + test( + 'uses the documented endpoint, current Basic credential, and OCS header', + () async { + late http.Request captured; + final revoker = NextcloudAppPasswordRevoker( + transport: _transport( + MockClient((request) async { + captured = request; + return http.Response('', 204); + }), + ), + ); + + await revoker.revoke( + accountId: 'nextcloud:alex', + credential: credential, + correlationId: 'revoke-1', + ); + + expect(captured.method, 'DELETE'); + expect( + captured.url, + Uri.parse( + 'https://cloud.example.test/nextcloud/ocs/v2.php/core/apppassword', + ), + ); + expect(captured.headers['ocs-apirequest'], 'true'); + expect(captured.headers['accept'], 'application/json'); + final authorization = captured.headers['authorization']!; + expect(authorization, startsWith('Basic ')); + expect( + utf8.decode(base64Decode(authorization.substring('Basic '.length))), + 'alex:app-secret', + ); + }, + ); + + for (final expectation in const [ + (status: 401, kind: DavErrorKind.authentication), + (status: 403, kind: DavErrorKind.authorization), + (status: 409, kind: DavErrorKind.protocol), + (status: 503, kind: DavErrorKind.server), + ]) { + test('maps ${expectation.status} and never retries revocation', () async { + var calls = 0; + final revoker = NextcloudAppPasswordRevoker( + transport: _transport( + MockClient((_) async { + calls += 1; + return http.Response('', expectation.status); + }), + ), + ); + + await expectLater( + revoker.revoke( + accountId: 'nextcloud:alex', + credential: credential, + correlationId: 'revoke-error', + ), + throwsA( + isA() + .having((error) => error.kind, 'kind', expectation.kind) + .having( + (error) => error.code, + 'code', + 'NextcloudAppPasswordRevocationFailed', + ) + .having( + (error) => error.statusCode, + 'statusCode', + expectation.status, + ) + .having( + (error) => error.correlationId, + 'correlationId', + 'revoke-error', + ), + ), + ); + expect(calls, 1); + }); + } +} + +DavHttpTransport _transport(http.Client client) => DavHttpTransport( + client: client, + profile: davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: Uri.parse('https://cloud.example.test/nextcloud'), + ), + accountAuthority: Uri.parse('https://cloud.example.test/nextcloud'), + delay: (_) async {}, +); diff --git a/test/dav/auth/nextcloud_login_flow_v2_test.dart b/test/dav/auth/nextcloud_login_flow_v2_test.dart new file mode 100644 index 0000000..df14839 --- /dev/null +++ b/test/dav/auth/nextcloud_login_flow_v2_test.dart @@ -0,0 +1,294 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/auth/nextcloud_login_flow_v2.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + test('normalizes HTTPS while preserving an installation path', () { + expect( + normalizeNextcloudLoginServer(' cloud.example.test/nextcloud/ '), + Uri.parse('https://cloud.example.test/nextcloud'), + ); + expect( + normalizeNextcloudLoginServer( + 'http://CLOUD.example.test:8443/nextcloud/', + ), + Uri.parse('https://cloud.example.test:8443/nextcloud'), + ); + expect( + () => normalizeNextcloudLoginServer('https://user@cloud.test/path'), + throwsFormatException, + ); + }); + + test('normalizes standard DAV endpoints to the installation root', () { + expect( + normalizeNextcloudLoginServer( + 'https://cloud.example.test/remote.php/dav', + ), + Uri.parse('https://cloud.example.test'), + ); + expect( + normalizeNextcloudLoginServer( + 'https://cloud.example.test/nextcloud/remote.php/dav/calendars/alex', + ), + Uri.parse('https://cloud.example.test/nextcloud'), + ); + expect( + normalizeNextcloudLoginServer( + 'https://cloud.example.test/nextcloud/remote.php/webdav/', + ), + Uri.parse('https://cloud.example.test/nextcloud'), + ); + }); + + test( + 'success treats 404 as pending and uses canonical returned identity', + () async { + final requests = []; + var polls = 0; + final client = MockClient((request) async { + requests.add(request); + if (request.url.path.endsWith('/index.php/login/v2')) { + return http.Response( + jsonEncode({ + 'poll': { + 'token': 'short-lived-token', + 'endpoint': + 'https://cloud.example.test/nextcloud/login/v2/poll', + }, + 'login': + 'https://cloud.example.test/nextcloud/login/v2/flow/browser', + }), + 200, + ); + } + polls += 1; + if (polls < 3) return http.Response('', 404); + return http.Response( + jsonEncode({ + 'server': 'https://CANONICAL.example.test/nextcloud/', + 'loginName': ' server-login ', + 'appPassword': ' app-secret ', + }), + 200, + ); + }); + Uri? opened; + final flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (uri) async { + opened = uri; + return true; + }, + delay: (_) async {}, + ); + + final result = await flow.start( + 'https://cloud.example.test/nextcloud/remote.php/dav', + ); + + expect(requests.first.url.path, '/nextcloud/index.php/login/v2'); + expect(opened?.path, '/nextcloud/login/v2/flow/browser'); + expect(polls, 3); + expect(requests.last.body, 'token=short-lived-token'); + expect(requests.last.url.query, isEmpty); + expect( + result.canonicalServer, + Uri.parse('https://canonical.example.test/nextcloud'), + ); + expect(result.loginName, 'server-login'); + expect(result.appPassword, 'app-secret'); + expect(result.toString(), isNot(contains('app-secret'))); + expect(result.toString(), isNot(contains('server-login'))); + expect(result.toString(), isNot(contains('canonical.example.test'))); + expect( + requests.every( + (request) => !request.url.toString().contains('short-lived-token'), + ), + isTrue, + ); + }, + ); + + test('cancel stops pending polling with a typed cancellation', () async { + late NextcloudLoginFlowV2 flow; + final client = MockClient((request) async { + if (request.url.path.endsWith('/index.php/login/v2')) { + return http.Response( + jsonEncode({ + 'poll': { + 'token': 'token', + 'endpoint': 'https://cloud.example.test/login/v2/poll', + }, + 'login': 'https://cloud.example.test/login/v2/flow/browser', + }), + 200, + ); + } + return http.Response('', 404); + }); + flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async { + flow.cancel(); + return true; + }, + delay: (_) async {}, + ); + + await expectLater( + flow.start('cloud.example.test'), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.cancelled, + ), + ), + ); + }); + + test('pending polling expires without becoming an auth rejection', () async { + var clock = DateTime.utc(2026, 8, 8, 12); + final client = MockClient((request) async { + if (request.url.path.endsWith('/index.php/login/v2')) { + return http.Response( + jsonEncode({ + 'poll': { + 'token': 'token', + 'endpoint': 'https://cloud.example.test/login/v2/poll', + }, + 'login': 'https://cloud.example.test/login/v2/flow/browser', + }), + 200, + ); + } + return http.Response('', 404); + }); + final flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async => true, + nowUtc: () => clock, + operationTimeout: const Duration(seconds: 2), + pollInterval: const Duration(seconds: 1), + delay: (duration) async => clock = clock.add(duration), + ); + + await expectLater( + flow.start('cloud.example.test'), + throwsA( + isA() + .having((error) => error.kind, 'kind', DavErrorKind.timeout) + .having((error) => error.code, 'code', 'NextcloudLoginFlowExpired'), + ), + ); + }); + + test('rejects cross-origin login and poll endpoints', () async { + final client = MockClient( + (_) async => http.Response( + jsonEncode({ + 'poll': { + 'token': 'token', + 'endpoint': 'https://attacker.example/login/v2/poll', + }, + 'login': 'https://cloud.example.test/login/v2/flow/browser', + }), + 200, + ), + ); + final flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async => true, + ); + + await expectLater( + flow.start('cloud.example.test'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'NextcloudLoginEndpointRejected', + ), + ), + ); + }); + + test('rejects cross-origin HTTP redirects before following them', () async { + var calls = 0; + final client = MockClient((_) async { + calls += 1; + return http.Response( + '', + 302, + headers: {'location': 'https://evil.test/'}, + ); + }); + final flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async => true, + ); + + await expectLater( + flow.start('cloud.example.test'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'NextcloudLoginRedirectRejected', + ), + ), + ); + expect(calls, 1); + }); + + test('browser launch failure and malformed success are terminal', () async { + final startBody = jsonEncode({ + 'poll': { + 'token': 'token', + 'endpoint': 'https://cloud.example.test/login/v2/poll', + }, + 'login': 'https://cloud.example.test/login/v2/flow/browser', + }); + final launchFailure = NextcloudLoginFlowV2( + client: MockClient((_) async => http.Response(startBody, 200)), + browserLauncher: (_) async => false, + ); + await expectLater( + launchFailure.start('cloud.example.test'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'NextcloudLoginBrowserLaunchFailed', + ), + ), + ); + + var calls = 0; + final malformed = NextcloudLoginFlowV2( + client: MockClient((_) async { + calls += 1; + return calls == 1 + ? http.Response(startBody, 200) + : http.Response('{bad json', 200); + }), + browserLauncher: (_) async => true, + delay: (_) async {}, + ); + await expectLater( + malformed.start('cloud.example.test'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'NextcloudLoginMalformedResponse', + ), + ), + ); + }); +} diff --git a/test/dav/dav_errors_test.dart b/test/dav/dav_errors_test.dart new file mode 100644 index 0000000..97e8e16 --- /dev/null +++ b/test/dav/dav_errors_test.dart @@ -0,0 +1,127 @@ +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('maps every required public DAV error category', () { + final cases = <(DavException, DavErrorCategory)>[ + ( + _error(DavErrorKind.authentication, 'DavAuthRejected'), + DavErrorCategory.davAuthRejected, + ), + ( + _error(DavErrorKind.authentication, 'DavCredentialsRevoked'), + DavErrorCategory.davCredentialsRevoked, + ), + ( + _error(DavErrorKind.tls, 'DavTlsFailure'), + DavErrorCategory.davTlsFailure, + ), + ( + _error(DavErrorKind.protocol, 'DavDiscoveryFailed'), + DavErrorCategory.davDiscoveryFailed, + ), + ( + _error(DavErrorKind.protocol, 'DavUnsupportedServer'), + DavErrorCategory.davUnsupportedServer, + ), + ( + _error(DavErrorKind.authorization, 'DavPermissionDenied'), + DavErrorCategory.davPermissionDenied, + ), + ( + _error(DavErrorKind.conflict, 'DavResourceConflict'), + DavErrorCategory.davResourceConflict, + ), + ( + _error(DavErrorKind.uidConflict, 'DavUidConflict'), + DavErrorCategory.davUidConflict, + ), + ( + _error(DavErrorKind.invalidCalendarData, 'DavMalformedResource'), + DavErrorCategory.davMalformedResource, + ), + ( + _error(DavErrorKind.unsupportedComponent, 'DavUnsupportedComponent'), + DavErrorCategory.davUnsupportedComponent, + ), + ( + _error(DavErrorKind.limitExceeded, 'DavQuotaOrSizeLimit'), + DavErrorCategory.davQuotaOrSizeLimit, + ), + ( + _error(DavErrorKind.rateLimited, 'DavRateLimited'), + DavErrorCategory.davRateLimited, + ), + ( + _error(DavErrorKind.network, 'DavNetworkFailure'), + DavErrorCategory.davTransientNetwork, + ), + ( + _error(DavErrorKind.server, 'DavServerUnavailable'), + DavErrorCategory.davServerUnavailable, + ), + ( + _error(DavErrorKind.invalidSyncToken, 'DavSyncTokenInvalid'), + DavErrorCategory.davSyncTokenInvalid, + ), + ( + _error(DavErrorKind.notFound, 'DavCollectionRemoved'), + DavErrorCategory.davCollectionRemoved, + ), + ( + _error(DavErrorKind.authorization, 'DavReadOnly'), + DavErrorCategory.davReadOnly, + ), + ( + _error(DavErrorKind.protocol, 'DavProtocolViolation'), + DavErrorCategory.davProtocolViolation, + ), + ]; + + for (final entry in cases) { + expect(entry.$1.category, entry.$2, reason: entry.$1.code); + } + }); + + test('exposes retry, user-action, and cached-data disposition', () { + final credentials = _error( + DavErrorKind.authentication, + 'DavCredentialsRevoked', + ); + expect(credentials.retryable, isFalse); + expect(credentials.requiresUserAction, isTrue); + expect(credentials.cachedDataUsable, isTrue); + + final unavailable = _error(DavErrorKind.server, 'DavServerUnavailable'); + expect(unavailable.retryable, isTrue); + expect(unavailable.requiresUserAction, isFalse); + expect(unavailable.cachedDataUsable, isTrue); + + final malformed = _error( + DavErrorKind.invalidCalendarData, + 'DavMalformedResource', + ); + expect(malformed.retryable, isFalse); + expect(malformed.requiresUserAction, isFalse); + expect(malformed.cachedDataUsable, isFalse); + }); + + test('parses bounded Retry-After delta seconds and HTTP dates', () { + final now = DateTime.utc(2026, 8, 8, 12); + + expect(parseDavRetryAfter('90', nowUtc: now), const Duration(seconds: 90)); + expect( + parseDavRetryAfter('Sat, 08 Aug 2026 12:00:30 GMT', nowUtc: now), + const Duration(seconds: 30), + ); + expect(parseDavRetryAfter('-1', nowUtc: now), isNull); + expect(parseDavRetryAfter('not-a-date', nowUtc: now), isNull); + expect( + parseDavRetryAfter('999999', nowUtc: now), + const Duration(hours: 24), + ); + }); +} + +DavException _error(DavErrorKind kind, String code) => + DavException(kind: kind, code: code, safeMessage: 'Safe message.'); diff --git a/test/dav/discovery/dav_discovery_test.dart b/test/dav/discovery/dav_discovery_test.dart new file mode 100644 index 0000000..a2e77f5 --- /dev/null +++ b/test/dav/discovery/dav_discovery_test.dart @@ -0,0 +1,343 @@ +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_repository.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_service.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + test( + 'discovers principal, home, mixed collections, ACLs, and safe HREFs', + () async { + final requests = []; + var requestIndex = 0; + final client = MockClient((request) async { + requests.add(request); + final response = switch (requestIndex) { + 0 => http.Response( + '', + 200, + headers: { + 'dav': '1, 3, calendar-access, sync-collection', + 'allow': 'OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE', + }, + ), + 1 => _multistatus(_currentPrincipalResponse), + 2 => _multistatus(_principalPropertiesResponse), + 3 => _multistatus(_inventoryResponse), + _ => throw StateError('Unexpected discovery request.'), + }; + requestIndex += 1; + return response; + }); + final authority = Uri.parse('https://cloud.example.test/nextcloud'); + final profile = davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: authority, + ); + final service = DavDiscoveryService( + transport: DavHttpTransport( + client: client, + profile: profile, + accountAuthority: authority, + delay: (_) async {}, + ), + profile: profile, + accountAuthority: authority, + accountId: 'account', + credential: DavBasicCredential( + username: 'alex', + password: 'app-secret', + ), + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ); + + late final DavDiscoveryResult result; + try { + result = await service.discover(correlationId: 'discover-1'); + } on Object catch (error) { + fail( + 'Discovery failed after ${requests.length} requests: $error; ' + 'targets=${requests.map((request) => request.url).join(',')}', + ); + } + + expect(requests.map((request) => request.method), [ + 'OPTIONS', + 'PROPFIND', + 'PROPFIND', + 'PROPFIND', + ]); + expect(requests[1].headers['depth'], '0'); + expect(requests[2].headers['depth'], '0'); + expect(requests[3].headers['depth'], '1'); + expect( + requests, + everyElement( + isA().having( + (request) => request.headers['authorization'], + 'authorization', + startsWith('Basic '), + ), + ), + ); + + expect( + result.service.principalHref.path, + '/nextcloud/remote.php/dav/principals/users/alex/', + ); + expect( + result.service.calendarHomeHref.path, + '/nextcloud/remote.php/dav/calendars/alex/', + ); + expect(result.service.calendarUserAddresses.single.scheme, 'mailto'); + expect(result.service.capabilities.hasSchedulingInbox, isTrue); + expect(result.collections, hasLength(3)); + + final work = result.collections.singleWhere( + (collection) => collection.displayName == 'Work & Team', + ); + expect( + work.hrefKey, + '/nextcloud/remote.php/dav/calendars/alex/Team%2FWork/', + ); + expect(work.kind, DavCollectionKind.mixedCalendar); + expect(work.eventProjectionEnabled, isTrue); + expect(work.taskProjectionEnabled, isTrue); + expect(work.capabilities.canCreateEvent, isTrue); + expect(work.capabilities.canCreateTask, isTrue); + expect(work.capabilities.supportsSyncCollection, isTrue); + expect(work.capabilities.supportsCalendarMultiget, isTrue); + expect(work.maximumResourceSize, 1048576); + expect(work.syncToken, 'https://cloud.example.test/token/opaque'); + expect(work.color, '#3584e4ff'); + expect(work.safeDisplayMetadata['owner-display-name'], 'Alex'); + + final subscribed = result.collections.singleWhere( + (collection) => collection.displayName == 'Subscribed', + ); + expect(subscribed.kind, DavCollectionKind.subscribedCalendar); + expect(subscribed.capabilities.isReadOnly, isTrue); + // An absent component-set means all CalDAV component types, not VEVENT. + expect(subscribed.eventProjectionEnabled, isTrue); + expect(subscribed.taskProjectionEnabled, isTrue); + + final inbox = result.collections.singleWhere( + (collection) => collection.kind == DavCollectionKind.schedulingInbox, + ); + expect(inbox.eventProjectionEnabled, isFalse); + expect(inbox.taskProjectionEnabled, isFalse); + }, + ); + + test( + 'successful inventory commits one collection and two projections', + () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + const nowText = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test/nextcloud', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + createdAtUtc: nowText, + updatedAtUtc: nowText, + ), + ); + var nextId = 0; + final repository = DavDiscoveryRepository( + database: database, + idFactory: () => 'collection-${nextId += 1}', + ); + final result = _repositoryDiscoveryResult(); + + await repository.commitSuccessfulInventory(result); + + final services = await database.select(database.davAccountServices).get(); + expect(services, hasLength(1)); + expect(services.single.capabilitiesJson, isNot(contains('app-secret'))); + final collections = await database.select(database.davCollections).get(); + expect(collections, hasLength(1)); + expect(collections.single.id, 'collection-1'); + expect(collections.single.readOnly, isFalse); + final sources = await database.select(database.calendarSources).get(); + final lists = await database.select(database.taskLists).get(); + expect(sources, hasLength(1)); + expect(lists, hasLength(1)); + expect(sources.single.davCollectionId, collections.single.id); + expect(lists.single.davCollectionId, collections.single.id); + + await repository.commitSuccessfulInventory( + DavDiscoveryResult( + accountId: result.accountId, + provider: result.provider, + service: result.service, + collections: const [], + ), + ); + final missing = await database + .select(database.davCollections) + .getSingle(); + expect(missing.serverMissing, isTrue); + expect( + (await database.select(database.calendarSources).getSingle()).isDeleted, + isTrue, + ); + expect( + (await database.select(database.taskLists).getSingle()).serverMissing, + isTrue, + ); + }, + ); +} + +http.Response _multistatus(String body) => http.Response( + body, + 207, + headers: {'content-type': 'application/xml; charset=utf-8'}, +); + +DavDiscoveryResult _repositoryDiscoveryResult() { + final now = DateTime.utc(2026, 8, 8, 12); + const capabilities = CollectionCapabilities( + canRead: true, + canWriteContent: true, + canAddMembers: true, + canDeleteMembers: true, + supportsEvents: true, + supportsTasks: true, + supportsSyncCollection: true, + supportsCalendarMultiget: true, + ); + return DavDiscoveryResult( + accountId: 'account', + provider: BusyProvider.nextcloud, + service: DavServiceDiscovery( + canonicalServiceUri: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/', + ), + canonicalOrigin: Uri.parse('https://cloud.example.test'), + principalHref: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/principals/users/alex/', + ), + calendarHomeHref: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/calendars/alex/', + ), + calendarUserAddresses: const [], + scheduleInboxHref: null, + scheduleOutboxHref: null, + capabilities: AccountServiceCapabilities( + hasPrincipal: true, + hasCalendarHome: true, + ), + discoveredAtUtc: now, + lastValidatedAtUtc: now, + providerProfileVersion: 1, + ), + collections: [ + DavCollectionDiscovery( + hrefKey: '/nextcloud/remote.php/dav/calendars/alex/work/', + requestUri: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/calendars/alex/work/', + ), + displayName: 'Work', + description: null, + resourceTypes: const {'{DAV:}collection', '{urn:test}calendar'}, + supportedComponentMask: davComponentEvent | davComponentTodo, + supportedCalendarData: const [ + {'contentType': 'text/calendar', 'version': '2.0'}, + ], + supportedReports: const {'{DAV:}sync-collection'}, + currentUserPrivileges: const {'{DAV:}read', '{DAV:}write-content'}, + ownerHref: '/principals/alex/', + safeDisplayMetadata: const {'owner-display-name': 'Alex'}, + color: '#3584e4ff', + sortOrder: 1, + calendarTimeZone: null, + calendarTimeZoneId: 'America/Vancouver', + scheduleTransparency: null, + maximumResourceSize: 1048576, + maximumInstances: null, + syncToken: 'opaque', + ctag: 'ctag', + capabilities: capabilities, + kind: DavCollectionKind.mixedCalendar, + eventProjectionEnabled: true, + taskProjectionEnabled: true, + ), + ], + ); +} + +const _currentPrincipalResponse = ''' + + /nextcloud/.well-known/caldav + /nextcloud/remote.php/dav/principals/users/alex/ + HTTP/1.1 200 OK +'''; + +const _principalPropertiesResponse = ''' + + /nextcloud/remote.php/dav/principals/users/alex/ + + /nextcloud/remote.php/dav/calendars/alex/ + mailto:alex@example.test + /nextcloud/remote.php/dav/calendars/alex/inbox/ + /nextcloud/remote.php/dav/calendars/alex/outbox/ + HTTP/1.1 200 OK + +'''; + +const _inventoryResponse = ''' + + /nextcloud/remote.php/dav/calendars/alex/ + + HTTP/1.1 200 OK + /nextcloud/remote.php/dav/calendars/alex/Team%2FWork/ + + + Work & Team + /nextcloud/remote.php/dav/principals/users/alex/ + + + + + + + + + + https://cloud.example.test/token/opaque + + + 10485761000 + #3584e4ff2 + Alex + HTTP/1.1 200 OK + + /nextcloud/remote.php/dav/calendars/alex/subscribed/ + + + Subscribed + + HTTP/1.1 200 OK + + /nextcloud/remote.php/dav/calendars/alex/inbox/ + + Inbox + HTTP/1.1 200 OK + +'''; diff --git a/test/dav/fake_dav_server_integration_test.dart b/test/dav/fake_dav_server_integration_test.dart new file mode 100644 index 0000000..86cff14 --- /dev/null +++ b/test/dav/fake_dav_server_integration_test.dart @@ -0,0 +1,438 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_service.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/mutation/dav_conditional_mutation_service.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/dav/sync/dav_collection_remote_client.dart'; +import 'package:busymax/src/dav/sync/dav_sync_engine.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'support/fake_dav_server.dart'; + +void main() { + late FakeDavServer server; + + setUp(() async { + server = FakeDavServer(); + await server.start(); + }); + + tearDown(() => server.close()); + + test( + 'socket discovery follows well-known redirect and observes ACL/removal changes', + () async { + var result = await _discover(server); + + expect(result.service.principalHref.path, server.principalPath); + expect(result.service.calendarHomeHref.path, server.calendarHomePath); + expect(result.collections, hasLength(1)); + expect(result.collections.single.hrefKey, server.collectionPath); + expect(result.collections.single.eventProjectionEnabled, isTrue); + expect(result.collections.single.taskProjectionEnabled, isTrue); + expect(result.collections.single.capabilities.canCreateEvent, isTrue); + expect(result.collections.single.capabilities.canCreateTask, isTrue); + expect(server.requests.map((request) => request.method).take(6), [ + 'OPTIONS', + 'OPTIONS', + 'PROPFIND', + 'PROPFIND', + 'PROPFIND', + ]); + expect( + server.requests, + everyElement( + isA().having( + (request) => request.hasBasicAuthorization, + 'Basic authorization present', + isTrue, + ), + ), + ); + + server.collectionReadOnly = true; + result = await _discover(server); + expect(result.collections.single.capabilities.isReadOnly, isTrue); + expect(result.collections.single.capabilities.canCreateEvent, isFalse); + expect(result.collections.single.capabilities.canCreateTask, isFalse); + + server.collectionRemoved = true; + result = await _discover(server); + expect(result.collections, isEmpty); + }, + ); + + test('RFC 6578 pages and multiget commit mixed VEVENT/VTODO data', () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + await _seedCollection(database, server); + var objectSequence = 0; + final objectRepository = DavObjectRepository( + database: database, + idFactory: () => 'object-${objectSequence += 1}', + ); + final result = + await DavSyncEngine( + database: database, + objectRepository: objectRepository, + remoteClient: _collectionClient(server), + accountId: 'account', + collectionId: 'collection', + provider: BusyProvider.nextcloud, + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ).synchronize( + correlationId: 'fake-sync', + projectionRangeStartUtc: DateTime.utc(2026, 1), + projectionRangeEndUtc: DateTime.utc(2027, 1), + ); + + expect(result.pages, 2); + expect(result.membersSeen, 2); + expect(result.objectsFetched, 2); + expect(result.finalCursorValue, server.finalSyncToken); + expect(await database.select(database.davObjects).get(), hasLength(2)); + expect(await database.select(database.calendarEvents).get(), hasLength(1)); + expect(await database.select(database.tasks).get(), hasLength(1)); + expect( + (await database.select(database.calendarEvents).getSingle()).title, + 'Server event', + ); + expect( + (await database.select(database.tasks).getSingle()).title, + 'Server task', + ); + final reports = server.requests + .where((request) => request.method == 'REPORT') + .toList(); + expect(reports, hasLength(3)); + expect( + reports.where((request) => request.body.contains('sync-collection')), + hasLength(2), + ); + expect( + reports + .singleWhere((request) => request.body.contains('calendar-multiget')) + .depth, + '1', + ); + + final inventory = await _collectionClient( + server, + ).listMemberEtags(correlationId: 'fake-inventory'); + expect(inventory.members, hasLength(2)); + }); + + test( + 'conditional writes handle ETag races, rewriting, and unknown outcome', + () async { + final client = DavMutationHttpClient( + transport: server.transport(), + accountId: 'account', + collectionId: 'collection', + credential: server.credential, + ); + final service = DavConditionalMutationService( + remoteClient: client, + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ); + final resource = server.resources[server.eventPath]!; + var baseline = resource.body; + var etag = resource.etag; + server.rewriteMutations = true; + + var result = await service.update( + hrefKey: server.eventPath, + uri: server.uriFor(server.eventPath), + baselineEtag: etag, + baselineRawIcs: baseline, + patch: _summaryPatch('Canonical update'), + capabilities: _eventMutationCapabilities, + correlationId: 'rewrite', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + expect( + result.canonicalObject!.rawIcsBody, + contains('SUMMARY:Canonical update'), + ); + expect( + result.canonicalObject!.rawIcsBody, + contains('X-SERVER-REWRITE:canonical'), + ); + expect( + server.requests + .singleWhere((request) => request.method == 'PUT') + .ifMatch, + etag, + ); + + baseline = server.resources[server.eventPath]!.body; + etag = server.resources[server.eventPath]!.etag; + server.raceNextMutation = true; + result = await service.update( + hrefKey: server.eventPath, + uri: server.uriFor(server.eventPath), + baselineEtag: etag, + baselineRawIcs: baseline, + patch: _summaryPatch('After ETag race'), + capabilities: _eventMutationCapabilities, + correlationId: 'race', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + expect( + result.canonicalObject!.rawIcsBody, + contains('SUMMARY:After ETag race'), + ); + + baseline = server.resources[server.eventPath]!.body; + etag = server.resources[server.eventPath]!.etag; + server + ..rewriteMutations = false + ..dropAfterNextMutation = true; + result = await service.update( + hrefKey: server.eventPath, + uri: server.uriFor(server.eventPath), + baselineEtag: etag, + baselineRawIcs: baseline, + patch: _summaryPatch('Applied before connection drop'), + capabilities: _eventMutationCapabilities, + correlationId: 'unknown-outcome', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + expect( + result.canonicalObject!.rawIcsBody, + contains('SUMMARY:Applied before connection drop'), + ); + }, + ); + + test( + 'status, per-resource, malformed, invalid-token, delay, and drop matrix', + () async { + final statusCases = <(int, DavErrorCategory)>[ + (401, DavErrorCategory.davAuthRejected), + (403, DavErrorCategory.davPermissionDenied), + (404, DavErrorCategory.davCollectionRemoved), + (409, DavErrorCategory.davResourceConflict), + (412, DavErrorCategory.davResourceConflict), + (423, DavErrorCategory.davResourceConflict), + (429, DavErrorCategory.davRateLimited), + (507, DavErrorCategory.davQuotaOrSizeLimit), + (500, DavErrorCategory.davServerUnavailable), + (503, DavErrorCategory.davServerUnavailable), + ]; + for (final entry in statusCases) { + server.enqueueFault( + FakeDavFault( + method: 'PROPFIND', + path: server.collectionPath, + statusCode: entry.$1, + headers: entry.$1 == 429 ? const {'retry-after': '17'} : const {}, + ), + ); + final error = await _captureDavError( + _collectionClient( + server, + limits: const DavTransportLimits(maximumReadAttempts: 1), + ).listMemberEtags(correlationId: 'status-${entry.$1}'), + ); + expect(error.category, entry.$2, reason: 'HTTP ${entry.$1}'); + if (entry.$1 == 429) { + expect(error.retryAfter, const Duration(seconds: 17)); + } + } + + server.resources[server.eventPath]!.multistatusStatus = 403; + var error = await _captureDavError( + _collectionClient( + server, + ).listMemberEtags(correlationId: 'member-status'), + ); + expect(error.category, DavErrorCategory.davPermissionDenied); + server.resources[server.eventPath]!.multistatusStatus = null; + + server.enqueueFault( + FakeDavFault( + method: 'PROPFIND', + path: server.collectionPath, + statusCode: 207, + body: '', + ), + ); + error = await _captureDavError( + _collectionClient(server).listMemberEtags(correlationId: 'malformed'), + ); + expect(error.category, DavErrorCategory.davProtocolViolation); + + error = await _captureDavError( + _collectionClient(server).syncCollectionPage( + syncToken: 'invalid-token', + correlationId: 'invalid-token', + ), + ); + expect(error.category, DavErrorCategory.davSyncTokenInvalid); + + server.enqueueFault( + FakeDavFault( + method: 'PROPFIND', + path: server.collectionPath, + delay: const Duration(milliseconds: 80), + statusCode: 207, + ), + ); + error = await _captureDavError( + _collectionClient( + server, + limits: const DavTransportLimits( + connectTimeout: Duration(milliseconds: 10), + maximumReadAttempts: 1, + ), + ).listMemberEtags(correlationId: 'delay'), + ); + expect(error.category, DavErrorCategory.davTransientNetwork); + + server.enqueueFault( + FakeDavFault( + method: 'PROPFIND', + path: server.collectionPath, + dropConnection: true, + ), + ); + error = await _captureDavError( + _collectionClient( + server, + limits: const DavTransportLimits(maximumReadAttempts: 1), + ).listMemberEtags(correlationId: 'drop'), + ); + expect(error.category, DavErrorCategory.davTransientNetwork); + }, + ); +} + +Future _discover(FakeDavServer server) => + DavDiscoveryService( + transport: server.transport(), + profile: server.profile, + accountAuthority: server.authority, + accountId: 'account', + credential: server.credential, + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ).discover(correlationId: 'fake-discovery'); + +DavCollectionHttpClient _collectionClient( + FakeDavServer server, { + DavTransportLimits limits = const DavTransportLimits(), +}) => DavCollectionHttpClient( + transport: server.transport(limits: limits), + profile: server.profile, + accountAuthority: server.authority, + accountId: 'account', + collectionId: 'collection', + collectionUri: server.uriFor(server.collectionPath), + credential: server.credential, +); + +Future _captureDavError(Future operation) async { + try { + await operation; + } on DavException catch (error) { + return error; + } + throw TestFailure('Expected a DavException.'); +} + +DavMutationPatch _summaryPatch(String summary) => DavMutationPatch( + target: const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + ), + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', summary)], +); + +const _eventMutationCapabilities = CollectionCapabilities( + canRead: true, + canWriteContent: true, + canAddMembers: true, + canDeleteMembers: true, + supportsEvents: true, +); + +Future _seedCollection(AppDatabase database, FakeDavServer server) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: server.authority.toString(), + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: server.collectionPath, + requestUri: server.uriFor(server.collectionPath).toString(), + displayName: 'Work & Tasks', + supportedComponentMask: const Value(3), + supportedReportsJson: Value( + jsonEncode([ + '{DAV:}sync-collection', + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ]), + ), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write-content']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-collection', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: server.collectionPath, + davCollectionId: const Value('collection'), + summary: 'Work & Tasks', + createdAtLocal: DateTime.utc(2026, 8, 8).millisecondsSinceEpoch, + updatedAtLocal: DateTime.utc(2026, 8, 8).millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-collection', + davCollectionId: const Value('collection'), + title: 'Work & Tasks', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} diff --git a/test/dav/http/dav_http_transport_test.dart b/test/dav/http/dav_http_transport_test.dart new file mode 100644 index 0000000..9e77108 --- /dev/null +++ b/test/dav/http/dav_http_transport_test.dart @@ -0,0 +1,292 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + const accountId = 'account'; + const correlationId = 'correlation-1'; + final credential = DavBasicCredential( + username: 'alex', + password: 'app-secret', + ); + + test('sends explicit UTF-8 bodies and preserves quoted ETags', () async { + late http.Request captured; + final transport = _nextcloudTransport( + MockClient((request) async { + captured = request; + return http.Response('stored', 200, headers: {'etag': '"abc-123"'}); + }), + ); + final response = await transport.send( + DavRequest.icalendar( + method: 'PUT', + uri: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/calendars/a/new.ics', + ), + accountId: accountId, + collectionId: 'collection', + correlationId: correlationId, + body: 'BEGIN:VCALENDAR\r\nSUMMARY:Résumé 📅\r\nEND:VCALENDAR\r\n', + headers: const {'if-match': '"old-etag"'}, + ), + credential: credential, + ); + + expect(captured.method, 'PUT'); + expect(captured.headers['content-type'], 'text/calendar; charset=utf-8'); + expect(captured.headers['if-match'], '"old-etag"'); + expect(captured.headers['authorization'], startsWith('Basic ')); + expect(utf8.decode(captured.bodyBytes), contains('Résumé 📅')); + expect(response.etag, '"abc-123"'); + expect(credential.toString(), isNot(contains('app-secret'))); + }); + + test( + 'rejects cross-origin redirect before credentials are forwarded', + () async { + final requested = []; + final transport = _nextcloudTransport( + MockClient((request) async { + requested.add(request.url); + return http.Response( + '', + 302, + headers: {'location': 'https://evil.example.test/steal'}, + ); + }), + ); + + await expectLater( + transport.send( + _propfind( + Uri.parse( + 'https://cloud.example.test/nextcloud/.well-known/caldav', + ), + ), + credential: credential, + ), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.redirectRejected, + ), + ), + ); + expect(requested, hasLength(1)); + }, + ); + + test( + 'allows approved iCloud shards but rejects look-alike domains', + () async { + final requested = []; + final headers = []; + final client = MockClient((request) async { + requested.add(request.url); + headers.add(request.headers['authorization']); + if (requested.length == 1) { + return http.Response( + '', + 301, + headers: {'location': 'https://p123-caldav.icloud.com/principal/'}, + ); + } + return http.Response('', 200); + }); + final profile = davProviderProfile(BusyProvider.appleICloud); + final transport = DavHttpTransport( + client: client, + profile: profile, + accountAuthority: Uri.parse('https://caldav.icloud.com'), + delay: (_) async {}, + random: Random(1), + ); + + final response = await transport.send( + _propfind(Uri.parse('https://caldav.icloud.com/.well-known/caldav')), + credential: credential, + ); + expect(response.requestUri.host, 'p123-caldav.icloud.com'); + expect(headers, everyElement(startsWith('Basic '))); + + final unsafe = DavHttpTransport( + client: MockClient( + (_) async => http.Response( + '', + 302, + headers: {'location': 'https://p1-caldav.icloud.com.evil.test/'}, + ), + ), + profile: profile, + accountAuthority: Uri.parse('https://caldav.icloud.com'), + delay: (_) async {}, + ); + await expectLater( + unsafe.send( + _propfind(Uri.parse('https://caldav.icloud.com/.well-known/caldav')), + credential: credential, + ), + throwsA(isA()), + ); + }, + ); + + test('retries safe reads but never blindly retries a mutation', () async { + var safeCalls = 0; + final safeTransport = _nextcloudTransport( + MockClient((_) async { + safeCalls += 1; + return safeCalls == 1 + ? http.Response('', 503, headers: {'retry-after': '0'}) + : http.Response('ok', 200); + }), + ); + final response = await safeTransport.send( + _propfind( + Uri.parse('https://cloud.example.test/nextcloud/remote.php/dav'), + ), + credential: credential, + ); + expect(response.statusCode, 200); + expect(safeCalls, 2); + + var mutationCalls = 0; + final mutationTransport = _nextcloudTransport( + MockClient((_) async { + mutationCalls += 1; + return http.Response('', 503); + }), + ); + final mutationResponse = await mutationTransport.send( + DavRequest.icalendar( + method: 'PUT', + uri: Uri.parse( + 'https://cloud.example.test/nextcloud/remote.php/dav/a.ics', + ), + accountId: accountId, + collectionId: 'collection', + correlationId: correlationId, + body: 'BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n', + headers: const {'if-none-match': '*'}, + ), + credential: credential, + ); + expect(mutationResponse.statusCode, 503); + expect(mutationCalls, 1); + }); + + test('honors delta-seconds and HTTP-date Retry-After values', () async { + final now = DateTime.utc(2026, 8, 8, 12); + final delays = []; + var calls = 0; + final transport = _nextcloudTransport( + MockClient((_) async { + calls += 1; + if (calls == 1) { + return http.Response( + '', + 503, + headers: { + 'retry-after': HttpDate.format( + now.add(const Duration(seconds: 23)), + ), + }, + ); + } + return http.Response('ok', 200); + }), + delay: (duration) async => delays.add(duration), + nowUtc: () => now, + ); + + final response = await transport.send( + _propfind( + Uri.parse('https://cloud.example.test/nextcloud/remote.php/dav'), + ), + credential: credential, + ); + + expect(response.statusCode, 200); + expect(delays, [const Duration(seconds: 23)]); + }); + + test('enforces response and cancellation bounds', () async { + final transport = _nextcloudTransport( + MockClient((_) async => http.Response('12345', 200)), + limits: const DavTransportLimits(maximumResponseBytes: 4), + ); + await expectLater( + transport.send( + _propfind( + Uri.parse('https://cloud.example.test/nextcloud/remote.php/dav'), + ), + credential: credential, + ), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.responseTooLarge, + ), + ), + ); + + final token = DavCancellationToken()..cancel(); + await expectLater( + transport.send( + _propfind( + Uri.parse('https://cloud.example.test/nextcloud/remote.php/dav'), + ), + credential: credential, + cancellationToken: token, + ), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.cancelled, + ), + ), + ); + }); +} + +DavHttpTransport _nextcloudTransport( + http.Client client, { + DavTransportLimits limits = const DavTransportLimits(), + DavDelay? delay, + DateTime Function()? nowUtc, +}) { + final authority = Uri.parse('https://cloud.example.test/nextcloud'); + return DavHttpTransport( + client: client, + profile: davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: authority, + ), + accountAuthority: authority, + limits: limits, + delay: delay ?? (_) async {}, + random: Random(1), + nowUtc: nowUtc, + ); +} + +DavRequest _propfind(Uri uri) => DavRequest.xml( + method: 'PROPFIND', + uri: uri, + accountId: 'account', + correlationId: 'correlation-1', + body: '', + headers: const {'depth': '0'}, +); diff --git a/test/dav/ical/ical_document_test.dart b/test/dav/ical/ical_document_test.dart new file mode 100644 index 0000000..6d12031 --- /dev/null +++ b/test/dav/ical/ical_document_test.dart @@ -0,0 +1,318 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/ical/ical_semantics.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('untouched parsing returns the exact original resource', () { + final source = _complexEvent.replaceAll('\r\n', '\n'); + final document = IcalDocument.parse(source); + + expect(document.serialize(), source); + expect(document.isDirty, isFalse); + }); + + test( + 'patching one property preserves recurrence set and unknown content', + () { + final semantic = IcalSemanticDocument.parse(_complexEvent); + final document = semantic.document; + final master = semantic.components.singleWhere( + (component) => component.recurrenceIdKey == null, + ); + expect(master.summary, 'Résumé planning 📅'); + expect(master.start?.kind, IcalTemporalKind.tzidDateTime); + expect(master.start?.timeZoneId, 'America/Vancouver'); + expect(master.recurrenceRules, ['FREQ=WEEKLY;COUNT=4;BYDAY=MO']); + expect(master.recurrenceDates, ['20260818T090000']); + expect(master.exceptionDates, ['20260811T090000']); + expect(master.alarms, hasLength(2)); + expect(master.extensionProperties['X-BUSYMAX-FUTURE'], ['opaque:value']); + expect(semantic.timeZones, hasLength(1)); + expect(semantic.buildIndex(), hasLength(2)); + + final attendee = master.documentComponent.firstProperty('ATTENDEE')!; + expect(attendee.parametersNamed('X-ROLE'), hasLength(2)); + expect(attendee.parameterValue('CN'), 'Doe, Jane: Lead; West'); + + IcalDocumentPatcher(document).replaceSingletonText( + const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event-uid@example.test', + ), + 'SUMMARY', + 'Updated résumé with a very long multi-byte title 📅📅📅📅📅📅📅📅📅📅', + ); + final serialized = document.serialize(); + final reparsed = IcalSemanticDocument.parse(serialized); + + expect(serialized, contains('X-IANA-UNKNOWN;VALUE=TEXT:keep-me')); + expect(serialized, contains('X-BUSYMAX-FUTURE:opaque:value')); + expect(serialized, contains('BEGIN:VTIMEZONE')); + expect('BEGIN:VALARM'.allMatches(serialized), hasLength(2)); + expect(serialized, contains('RECURRENCE-ID;TZID=America/Vancouver')); + expect(serialized, contains('X-ROLE=PRIMARY;X-ROLE=SECONDARY')); + expect(serialized, contains('CN="Doe, Jane: Lead; West"')); + expect(reparsed.components, hasLength(2)); + expect( + reparsed.components + .singleWhere((component) => component.recurrenceIdKey == null) + .summary, + startsWith('Updated résumé'), + ); + for (final line + in serialized.split('\r\n').where((line) => line.isNotEmpty)) { + expect(utf8.encode(line).length, lessThanOrEqualTo(75), reason: line); + } + }, + ); + + test('repeated replacements do not remove unrelated repeated properties', () { + final semantic = IcalSemanticDocument.parse(_complexEvent); + final patcher = IcalDocumentPatcher(semantic.document); + const key = IcalComponentKey( + componentType: 'VEVENT', + uid: 'event-uid@example.test', + ); + patcher.replaceRepeatedRaw(key, 'CATEGORIES', [ + (value: r'Updated\, category', parameters: const []), + (value: 'Second', parameters: const []), + ]); + patcher.replaceSingletonText(key, 'LOCATION', null); + final result = IcalSemanticDocument.parse(semantic.document.serialize()); + final master = result.components.first; + + expect(master.categories, ['Updated, category', 'Second']); + expect(master.location, equals(null)); + expect(master.attendees, hasLength(1)); + expect(master.alarms, hasLength(2)); + }); + + test('all-day values remain dates with exclusive DTEND', () { + final component = IcalSemanticDocument.parse( + _allDayEvent, + ).components.single; + + expect(component.start?.kind, IcalTemporalKind.date); + expect(component.start?.rawValue, '20260808'); + expect(component.end?.kind, IcalTemporalKind.date); + expect(component.end?.rawValue, '20260810'); + }); + + test( + 'VTODO semantics preserve hierarchy, priority, progress, and extensions', + () { + final semantic = IcalSemanticDocument.parse(_taskResource); + final task = semantic.components.single; + + expect(task.componentType, 'VTODO'); + expect(task.parentUid, 'parent-uid'); + expect(task.priority, 7); + expect(task.sortOrder, 42); + expect(task.percentComplete, 50); + expect(task.taskUiState, IcalTaskUiState.inProgress); + expect(task.extensionProperties['X-PINNED'], ['1']); + expect(task.extensionProperties['X-OC-HIDESUBTASKS'], ['1']); + expect( + task.documentComponent.propertiesNamed('RELATED-TO'), + hasLength(3), + ); + expect(semantic.document.serialize(), _taskResource); + }, + ); + + test('percent complete alone does not close a Nextcloud task', () { + final semantic = IcalSemanticDocument.parse('''BEGIN:VCALENDAR\r +VERSION:2.0\r +BEGIN:VTODO\r +UID:percent-only@example.test\r +SUMMARY:Percent only\r +PERCENT-COMPLETE:100\r +END:VTODO\r +END:VCALENDAR\r +'''); + + expect(semantic.components.single.taskUiState, IcalTaskUiState.open); + }); + + test('semantic hashes ignore folding and property ordering', () { + final first = IcalSemanticDocument.parse(_allDayEvent); + final second = IcalSemanticDocument.parse('''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +SUMMARY:All-day event\r +DTEND;VALUE=DATE:20260810\r +UID:all-day@example.test\r +DTSTART;VALUE=DATE:20260808\r +DTSTAMP:20260801T120000Z\r +END:VEVENT\r +END:VCALENDAR\r +'''); + + expect(first.semanticHash, second.semanticHash); + }); + + test('rejects malformed structures, invalid dates, and mixed UIDs', () { + expect( + () => IcalDocument.parse('BEGIN:VCALENDAR\r\nEND:VEVENT\r\n'), + throwsA(isA()), + ); + expect( + () => IcalSemanticDocument.parse( + _allDayEvent.replaceFirst('20260808', '20261340'), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalInvalidTemporalValue', + ), + ), + ); + expect( + () => IcalSemanticDocument.parse( + _complexEvent.replaceFirst( + 'RECURRENCE-ID;TZID=America/Vancouver:20260818T090000', + 'UID:different@example.test\r\n' + 'RECURRENCE-ID;TZID=America/Vancouver:20260818T090000', + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalCalendarObjectInvariantFailed', + ), + ), + ); + }); + + test('new resources include valid framing and text escaping', () { + final component = IcalComponent( + name: 'VTODO', + children: [ + _property('UID', 'new-task@example.test'), + _property('DTSTAMP', '20260808T120000Z'), + _property('SUMMARY', encodeIcalText('One, two; three\nnext')), + ], + originalBeginLine: 'BEGIN:VTODO', + originalEndLine: 'END:VTODO', + structurallyDirty: true, + ); + final document = IcalDocument.create(components: [component]); + final serialized = document.serialize(); + final parsed = IcalSemanticDocument.parse(serialized).components.single; + + expect(serialized, startsWith('BEGIN:VCALENDAR\r\n')); + expect(serialized, endsWith('END:VCALENDAR\r\n')); + expect(serialized, contains('VERSION:2.0')); + expect(parsed.summary, 'One, two; three\nnext'); + }); +} + +IcalProperty _property(String name, String value) => IcalProperty( + group: null, + name: name, + parameters: const [], + rawValue: value, + originalPhysicalLines: const [], + isDirty: true, +); + +const _complexEvent = '''BEGIN:VCALENDAR\r +PRODID:-//BusyMax Test//EN\r +VERSION:2.0\r +X-WR-CALNAME:Preserve this\r +BEGIN:VTIMEZONE\r +TZID:America/Vancouver\r +X-LIC-LOCATION:America/Vancouver\r +BEGIN:STANDARD\r +DTSTART:19701101T020000\r +TZOFFSETFROM:-0700\r +TZOFFSETTO:-0800\r +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r +END:STANDARD\r +END:VTIMEZONE\r +BEGIN:VEVENT\r +UID:event-uid@example.test\r +DTSTAMP:20260801T120000Z\r +SEQUENCE:3\r +DTSTART;TZID=America/Vancouver:20260804T090000\r +DTEND;TZID=America/Vancouver:20260804T100000\r +SUMMARY:Résumé planning 📅\r +DESCRIPTION:Line one\\nLine two\\, retained\r +LOCATION:Office\r +RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO\r +RDATE;TZID=America/Vancouver:20260818T090000\r +EXDATE;TZID=America/Vancouver:20260811T090000\r +CATEGORIES:Planning,Team\r +ATTENDEE;X-ROLE=PRIMARY;X-ROLE=SECONDARY;CN="Doe, Jane: Lead; West":mailto:jane@example.test\r +ORGANIZER;CN=Alex:mailto:alex@example.test\r +X-IANA-UNKNOWN;VALUE=TEXT:keep-me\r +X-BUSYMAX-FUTURE:opaque:value\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT15M\r +DESCRIPTION:Reminder\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +ATTACH;VALUE=URI:Basso\r +END:VALARM\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:event-uid@example.test\r +RECURRENCE-ID;TZID=America/Vancouver:20260818T090000\r +DTSTAMP:20260802T120000Z\r +DTSTART;TZID=America/Vancouver:20260818T110000\r +DTEND;TZID=America/Vancouver:20260818T120000\r +SUMMARY:Moved occurrence\r +X-EXCEPTION-UNKNOWN:retain\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +const _allDayEvent = '''BEGIN:VCALENDAR\r +PRODID:-//BusyMax Test//EN\r +VERSION:2.0\r +BEGIN:VEVENT\r +UID:all-day@example.test\r +DTSTAMP:20260801T120000Z\r +DTSTART;VALUE=DATE:20260808\r +DTEND;VALUE=DATE:20260810\r +SUMMARY:All-day event\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +const _taskResource = '''BEGIN:VCALENDAR\r +PRODID:-//Nextcloud Tasks//EN\r +VERSION:2.0\r +BEGIN:VTODO\r +UID:task-uid\r +DTSTAMP:20260808T120000Z\r +SUMMARY:Task title\r +DESCRIPTION:Keep details\r +DTSTART;TZID=America/Vancouver:20260808T090000\r +DUE;TZID=America/Vancouver:20260809T170000\r +STATUS:IN-PROCESS\r +PERCENT-COMPLETE:50\r +PRIORITY:7\r +RELATED-TO:parent-uid\r +RELATED-TO;RELTYPE=CHILD:child-uid\r +RELATED-TO;RELTYPE=SIBLING:sibling-uid\r +X-APPLE-SORT-ORDER:42\r +X-PINNED:1\r +X-OC-HIDESUBTASKS:1\r +X-OC-HIDECOMPLETEDSUBTASKS:0\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT30M\r +END:VALARM\r +END:VTODO\r +END:VCALENDAR\r +'''; diff --git a/test/dav/ical/ical_recurrence_test.dart b/test/dav/ical/ical_recurrence_test.dart new file mode 100644 index 0000000..751e687 --- /dev/null +++ b/test/dav/ical/ical_recurrence_test.dart @@ -0,0 +1,336 @@ +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/ical/ical_recurrence.dart'; +import 'package:busymax/src/dav/ical/ical_semantics.dart'; +import 'package:busymax/src/dav/ical/ical_timezone.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('expands weekly recurrence with RDATE, EXDATE, and moved exception', () { + final document = IcalSemanticDocument.parse('''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:weekly@example.test\r +DTSTART;TZID=America/Vancouver:20260803T090000\r +DTEND;TZID=America/Vancouver:20260803T100000\r +RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO\r +RDATE;TZID=America/Vancouver:20260907T090000\r +EXDATE;TZID=America/Vancouver:20260810T090000\r +SUMMARY:Master\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:weekly@example.test\r +RECURRENCE-ID;TZID=America/Vancouver:20260817T090000\r +DTSTART;TZID=America/Vancouver:20260818T110000\r +DTEND;TZID=America/Vancouver:20260818T123000\r +SUMMARY:Moved\r +END:VEVENT\r +END:VCALENDAR\r +'''); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 8), + rangeEndUtc: DateTime.utc(2026, 10), + ); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20260803T090000', + '20260818T110000', + '20260824T090000', + '20260907T090000', + ]); + final moved = occurrences.singleWhere( + (occurrence) => occurrence.isException, + ); + expect(moved.recurrenceId.rawValue, '20260817T090000'); + expect(moved.end?.rawValue, '20260818T123000'); + expect(moved.summary, 'Moved'); + }); + + test('preserves wall time across a TZID daylight-saving transition', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART;TZID=America/Vancouver:20260301T090000', + end: 'DTEND;TZID=America/Vancouver:20260301T100000', + rule: 'RRULE:FREQ=WEEKLY;COUNT=3', + ), + ); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 2, 28), + rangeEndUtc: DateTime.utc(2026, 3, 20), + ); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20260301T090000', + '20260308T090000', + '20260315T090000', + ]); + expect(occurrences.map((occurrence) => occurrence.end!.rawValue), [ + '20260301T100000', + '20260308T100000', + '20260315T100000', + ]); + }); + + test('resolves an embedded custom VTIMEZONE across daylight saving', () { + final document = IcalSemanticDocument.parse('''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VTIMEZONE\r +TZID:Custom/Pacific-Test\r +BEGIN:STANDARD\r +DTSTART:19701101T020000\r +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r +TZOFFSETFROM:-0700\r +TZOFFSETTO:-0800\r +END:STANDARD\r +BEGIN:DAYLIGHT\r +DTSTART:19700308T020000\r +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r +TZOFFSETFROM:-0800\r +TZOFFSETTO:-0700\r +END:DAYLIGHT\r +END:VTIMEZONE\r +BEGIN:VEVENT\r +UID:custom-zone@example.test\r +DTSTART;TZID=Custom/Pacific-Test:20260301T090000\r +DTEND;TZID=Custom/Pacific-Test:20260301T100000\r +RRULE:FREQ=WEEKLY;COUNT=3\r +SUMMARY:Custom zone\r +END:VEVENT\r +END:VCALENDAR\r +'''); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 2, 28), + rangeEndUtc: DateTime.utc(2026, 3, 20), + ); + final resolver = IcalTimeZoneResolver.fromDocument(document); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20260301T090000', + '20260308T090000', + '20260315T090000', + ]); + expect(occurrences.map((occurrence) => resolver.toUtc(occurrence.start)), [ + DateTime.utc(2026, 3, 1, 17), + DateTime.utc(2026, 3, 8, 16), + DateTime.utc(2026, 3, 15, 16), + ]); + }); + + test('does not guess an unresolved custom TZID', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART;TZID=Private/Unknown:20260301T090000', + end: 'DTEND;TZID=Private/Unknown:20260301T100000', + rule: 'RRULE:FREQ=WEEKLY;COUNT=2', + ), + ); + + expect( + () => IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 2, 28), + rangeEndUtc: DateTime.utc(2026, 3, 20), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalUnknownTimeZone', + ), + ), + ); + }); + + test('supports monthly ordinal days and BYSETPOS', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART:20260130T090000Z', + end: 'DTEND:20260130T100000Z', + rule: 'RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1', + ), + ); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026), + rangeEndUtc: DateTime.utc(2026, 6), + ); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20260130T090000Z', + '20260227T090000Z', + '20260331T090000Z', + '20260430T090000Z', + ]); + }); + + test('supports yearly BYMONTH and ordinal BYDAY', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART:20261126T090000Z', + end: 'DTEND:20261126T100000Z', + rule: 'RRULE:FREQ=YEARLY;COUNT=3;BYMONTH=11;BYDAY=4TH', + ), + ); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026), + rangeEndUtc: DateTime.utc(2029), + ); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20261126T090000Z', + '20271125T090000Z', + '20281123T090000Z', + ]); + }); + + test('all-day occurrence keeps DATE values and exclusive duration', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART;VALUE=DATE:20260808', + end: 'DTEND;VALUE=DATE:20260810', + rule: 'RRULE:FREQ=DAILY;COUNT=2', + ), + ); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 8, 8), + rangeEndUtc: DateTime.utc(2026, 8, 12), + ); + + expect(occurrences.map((occurrence) => occurrence.start.rawValue), [ + '20260808', + '20260809', + ]); + expect(occurrences.map((occurrence) => occurrence.end!.rawValue), [ + '20260810', + '20260811', + ]); + expect( + occurrences.every( + (occurrence) => occurrence.start.kind == IcalTemporalKind.date, + ), + isTrue, + ); + }); + + test('cancelled exception is retained as an explicit occurrence state', () { + final document = IcalSemanticDocument.parse('''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:cancelled@example.test\r +DTSTART:20260801T090000Z\r +DURATION:PT1H\r +RRULE:FREQ=DAILY;COUNT=2\r +SUMMARY:Master\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:cancelled@example.test\r +RECURRENCE-ID:20260802T090000Z\r +STATUS:CANCELLED\r +END:VEVENT\r +END:VCALENDAR\r +'''); + + final occurrences = IcalRecurrenceExpander().expand( + document, + rangeStartUtc: DateTime.utc(2026, 8), + rangeEndUtc: DateTime.utc(2026, 8, 4), + ); + + expect(occurrences, hasLength(2)); + expect(occurrences.last.isCancelled, isTrue); + expect(occurrences.last.start.rawValue, '20260802T090000Z'); + expect(occurrences.last.end?.rawValue, '20260802T100000Z'); + }); + + test('bounds projection range, output, and malformed rule input', () { + final document = IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART:20260101T000000Z', + end: 'DTEND:20260101T000001Z', + rule: 'RRULE:FREQ=SECONDLY', + ), + ); + final expander = IcalRecurrenceExpander( + limits: const IcalRecurrenceLimits( + maximumOccurrences: 5, + maximumProjectionRange: Duration(days: 10), + ), + ); + + expect( + () => expander.expand( + document, + rangeStartUtc: DateTime.utc(2026), + rangeEndUtc: DateTime.utc(2026, 1, 2), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalRecurrenceOccurrenceLimitExceeded', + ), + ), + ); + expect( + () => IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART:20260101T000000Z', + end: 'DTEND:20260101T000001Z', + rule: 'RRULE:FREQ=DAILY;BYMONTHDAY=0', + ), + ), + returnsNormally, + ); + expect( + () => IcalRecurrenceExpander().expand( + IcalSemanticDocument.parse( + _eventWithRule( + start: 'DTSTART:20260101T000000Z', + end: 'DTEND:20260101T000001Z', + rule: 'RRULE:FREQ=DAILY;BYMONTHDAY=0', + ), + ), + rangeStartUtc: DateTime.utc(2026), + rangeEndUtc: DateTime.utc(2026, 1, 2), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalInvalidRecurrenceRule', + ), + ), + ); + }); +} + +String _eventWithRule({ + required String start, + required String end, + required String rule, +}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:rule@example.test\r +$start\r +$end\r +$rule\r +SUMMARY:Rule\r +END:VEVENT\r +END:VCALENDAR\r +'''; diff --git a/test/dav/ical/ical_task_alarm_test.dart b/test/dav/ical/ical_task_alarm_test.dart new file mode 100644 index 0000000..578f25d --- /dev/null +++ b/test/dav/ical/ical_task_alarm_test.dart @@ -0,0 +1,160 @@ +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/ical/ical_task_alarm.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('relative DISPLAY alarm uses the RFC 5545 due-date relation', () { + final alarm = IcalTaskAlarm.displayRelative( + const Duration(hours: -2), + relatedToDue: true, + ); + final component = alarm.toComponent(); + + expect(alarm.action, 'DISPLAY'); + expect(alarm.isRelative, isTrue); + expect(alarm.isRelatedToDue, isTrue); + expect(alarm.relativeOffset, const Duration(hours: -2)); + expect(component.firstProperty('TRIGGER')?.rawValue, '-PT2H'); + expect( + component.firstProperty('TRIGGER')?.parameterValue('RELATED'), + 'END', + ); + expect( + component.firstProperty('DESCRIPTION')?.decodedTextValue, + 'This is a todo reminder.', + ); + }); + + test('absolute DISPLAY alarm is stored as a UTC DATE-TIME', () { + final alarm = IcalTaskAlarm.displayAbsolute( + DateTime.parse('2026-08-09T10:15:30-07:00'), + ); + + expect(alarm.isAbsolute, isTrue); + expect(alarm.absoluteUtc, DateTime.utc(2026, 8, 9, 17, 15, 30)); + expect(alarm.triggerRaw, '20260809T171530Z'); + expect(alarm.canEditTrigger, isTrue); + expect(alarm.canEditTriggerFor(allDay: false), isTrue); + }); + + test('matches Nextcloud relative-alarm editing restrictions', () { + final beforeStart = IcalTaskAlarm.displayRelative( + const Duration(minutes: -10), + relatedToDue: false, + ); + final afterStart = IcalTaskAlarm.displayRelative( + const Duration(hours: 9), + relatedToDue: false, + ); + final afterStartNextDay = IcalTaskAlarm.displayRelative( + const Duration(hours: 25), + relatedToDue: false, + ); + final beforeDue = IcalTaskAlarm.displayRelative( + const Duration(minutes: -10), + relatedToDue: true, + ); + + expect(beforeStart.canEditTriggerFor(allDay: false), isTrue); + expect(afterStart.canEditTriggerFor(allDay: false), isFalse); + expect(afterStart.canEditTriggerFor(allDay: true), isTrue); + expect(afterStartNextDay.canEditTriggerFor(allDay: true), isFalse); + expect(beforeDue.canEditTriggerFor(allDay: false), isFalse); + expect(beforeDue.canEditTriggerFor(allDay: true), isFalse); + }); + + test( + 'unsupported alarm properties survive JSON and component round trips', + () { + final imported = IcalTaskAlarm.fromComponent( + IcalComponent( + name: 'VALARM', + children: [ + _property('ACTION', 'AUDIO'), + _property('TRIGGER', '-PT5M'), + _property( + 'ATTACH', + 'https://cloud.example.test/chime.ogg', + parameters: const [ + IcalParameter( + name: 'FMTTYPE', + values: ['audio/ogg'], + wasQuoted: false, + ), + ], + ), + _property('X-NEXTCLOUD-UNKNOWN', 'opaque'), + ], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + ), + ); + final decoded = decodeIcalTaskAlarms(encodeIcalTaskAlarms([imported])); + final component = decoded.single.toComponent(); + + expect(decoded.single, imported); + expect(component.firstProperty('ACTION')?.rawValue, 'AUDIO'); + expect( + component.firstProperty('ATTACH')?.parameterValue('FMTTYPE'), + 'audio/ogg', + ); + expect( + component.firstProperty('X-NEXTCLOUD-UNKNOWN')?.rawValue, + 'opaque', + ); + }, + ); + + test('malformed imported trigger remains visible but is not editable', () { + final alarm = IcalTaskAlarm.fromComponent( + IcalComponent( + name: 'VALARM', + children: [_property('ACTION', 'EMAIL'), _property('TRIGGER', 'bad')], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + ), + ); + final decoded = decodeIcalTaskAlarms(encodeIcalTaskAlarms([alarm])).single; + + expect(decoded.triggerRaw, 'bad'); + expect(decoded.isRelative, isFalse); + expect(decoded.isAbsolute, isFalse); + expect(decoded.canEditTrigger, isFalse); + }); + + test('all-day reminder offsets round-trip through day and time fields', () { + const samples = [ + Duration(hours: 9), + Duration(hours: -15), + Duration(hours: -39), + Duration(hours: -159), + Duration(days: -1), + Duration.zero, + ]; + + for (final sample in samples) { + final fields = IcalAllDayAlarmOffset.fromDuration(sample); + expect(fields.toDuration(), sample, reason: '$sample'); + } + + final week = IcalAllDayAlarmOffset.fromDuration( + const Duration(hours: -159), + ); + expect(week.amount, 1); + expect(week.unit, IcalAllDayAlarmUnit.weeks); + expect(week.hour, 9); + expect(week.minute, 0); + }); +} + +IcalProperty _property( + String name, + String value, { + List parameters = const [], +}) => IcalProperty( + group: null, + name: name, + parameters: parameters, + rawValue: value, + originalPhysicalLines: const [], +); diff --git a/test/dav/ical/ical_task_recurrence_test.dart b/test/dav/ical/ical_task_recurrence_test.dart new file mode 100644 index 0000000..5a2e0d7 --- /dev/null +++ b/test/dav/ical/ical_task_recurrence_test.dart @@ -0,0 +1,122 @@ +import 'package:busymax/src/dav/ical/ical_task_recurrence.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('parses and serializes the Nextcloud recurrence editor fields', () { + final recurrence = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=YEARLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR;BYMONTH=1,6;BYSETPOS=-1;COUNT=8"],"dates":["20260809"],"excludedDates":["20270809"]}''', + ); + + expect(recurrence.isSupported, isTrue); + expect(recurrence.frequency, IcalTaskRecurrenceFrequency.yearly); + expect(recurrence.interval, 2); + expect(recurrence.byDay, ['MO', 'TU', 'WE', 'TH', 'FR']); + expect(recurrence.byMonth, [1, 6]); + expect(recurrence.bySetPosition, -1); + expect(recurrence.count, 8); + expect(recurrence.recurrenceDates, ['20260809']); + expect(recurrence.exceptionDates, ['20270809']); + expect( + recurrence.toRrule(), + 'FREQ=YEARLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR;BYMONTH=1,6;BYSETPOS=-1;COUNT=8', + ); + }); + + test('all-day and timed UNTIL values are inclusive wire values', () { + const monthly = IcalTaskRecurrence( + frequency: IcalTaskRecurrenceFrequency.monthly, + interval: 1, + byDay: [], + byMonth: [], + byMonthDay: [15], + bySetPosition: null, + count: null, + untilRaw: null, + recurrenceDates: [], + exceptionDates: [], + rawRules: [], + isSupported: true, + ); + + expect( + monthly.withUntilDate('2026-12-31', allDay: true).untilRaw, + '20261231', + ); + final timed = monthly.withUntilDate('2026-12-31', allDay: false); + expect(timed.untilRaw, matches(RegExp(r'^\d{8}T\d{6}Z$'))); + expect(timed.untilDate, '2026-12-31'); + }); + + test('multiple or unknown rules remain opaque and are never rewritten', () { + final multiple = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=DAILY","FREQ=WEEKLY"],"dates":[],"excludedDates":[]}''', + ); + final unsupported = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=HOURLY;INTERVAL=3"],"dates":[],"excludedDates":[]}''', + ); + + expect(multiple.isSupported, isFalse); + expect(multiple.rawRules, ['FREQ=DAILY', 'FREQ=WEEKLY']); + expect(unsupported.isSupported, isFalse); + expect(unsupported.rawRules, ['FREQ=HOURLY;INTERVAL=3']); + expect(() => unsupported.toRrule(), throwsStateError); + }); + + test('invalid BYDAY ordinals are preserved as unsupported', () { + final recurrence = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=MONTHLY;BYDAY=+MO"],"dates":[],"excludedDates":[]}''', + ); + + expect(recurrence.isSupported, isFalse); + expect(recurrence.rawRules, ['FREQ=MONTHLY;BYDAY=+MO']); + }); + + test('normalizes the ordinal BYDAY form accepted by Nextcloud', () { + final recurrence = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=MONTHLY;BYDAY=-1FR"],"dates":[],"excludedDates":[]}''', + ); + + expect(recurrence.isSupported, isTrue); + expect(recurrence.byDay, ['FR']); + expect(recurrence.bySetPosition, -1); + expect( + recurrence.toRrule(), + 'FREQ=MONTHLY;INTERVAL=1;BYDAY=FR;BYSETPOS=-1', + ); + }); + + test('fills omitted weekly and yearly values from the task base date', () { + final weekly = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=WEEKLY"],"dates":[],"excludedDates":[]}''', + baseDate: DateTime(2026, 8, 9), + ); + final yearly = IcalTaskRecurrence.fromJson( + '''{"rules":["FREQ=YEARLY"],"dates":[],"excludedDates":[]}''', + baseDate: DateTime(2026, 8, 9), + ); + + expect(weekly.byDay, ['SU']); + expect(yearly.byMonth, [8]); + expect(yearly.byMonthDay, [9]); + }); + + test('preserves values outside the Nextcloud editor limits', () { + const rules = [ + 'FREQ=DAILY;BYDAY=MO', + 'FREQ=WEEKLY;INTERVAL=367;BYDAY=MO', + 'FREQ=MONTHLY;BYMONTHDAY=-1', + 'FREQ=MONTHLY;BYDAY=MO,TU;BYSETPOS=1', + 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-3', + 'FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1;COUNT=3501', + ]; + + for (final rule in rules) { + final recurrence = IcalTaskRecurrence.fromJson( + '{"rules":["$rule"],"dates":[],"excludedDates":[]}', + baseDate: DateTime(2026, 8, 9), + ); + expect(recurrence.isSupported, isFalse, reason: rule); + expect(recurrence.rawRules, [rule], reason: rule); + } + }); +} diff --git a/test/dav/mutation/dav_conditional_mutation_service_test.dart b/test/dav/mutation/dav_conditional_mutation_service_test.dart new file mode 100644 index 0000000..4b04c23 --- /dev/null +++ b/test/dav/mutation/dav_conditional_mutation_service_test.dart @@ -0,0 +1,666 @@ +import 'dart:math'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/mutation/dav_conditional_mutation_service.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/sync/dav_collection_remote_client.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + test('new resource factory uses distinct opaque filename and RFC UID', () { + final ids = ['uid-value', 'filename-value']; + final factory = DavNewObjectFactory( + idFactory: () => ids.removeAt(0), + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ); + + final task = factory.task(summary: 'Title only'); + + expect(task.uid, 'uid-value@busymax.local'); + expect(task.initialMemberName, 'filename-value.ics'); + expect(task.initialMemberName, isNot(contains('Title'))); + expect(task.rawIcs, contains('BEGIN:VTODO')); + expect(task.rawIcs, contains('UID:uid-value@busymax.local')); + expect(task.rawIcs, contains('DTSTAMP:20260808T120000Z')); + }); + + test( + 'create uses If-None-Match semantics and bounds filename collisions', + () async { + final putUris = []; + var puts = 0; + late String stored; + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + putUris.add(uri); + expect(ifNoneMatch, isTrue); + expect(ifMatch, isNull); + puts += 1; + if (puts == 1) return _precondition; + stored = rawIcs; + return _success; + }, + fetcher: (href, uri) async => _live(href, uri, '"created"', stored), + ); + final service = DavConditionalMutationService( + remoteClient: remote, + memberIdFactory: () => 'collision-retry', + ); + final object = DavNewObject( + uid: 'new@example.test', + initialMemberName: 'first.ics', + rawIcs: _event('Baseline'), + componentType: 'VEVENT', + ); + + final result = await service.create( + collectionUri: _collectionUri, + object: object, + capabilities: _writable, + correlationId: 'create', + ); + + expect(result.outcome, DavMutationOutcome.succeeded); + expect(putUris.map((uri) => uri.path), [ + '${_collectionUri.path}first.ics', + '${_collectionUri.path}collision-retry.ics', + ]); + expect(result.canonicalObject?.etag, '"created"'); + }, + ); + + test( + 'lost create response is resolved by GET and never blindly repeated', + () async { + var puts = 0; + final intended = _event('Created'); + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + puts += 1; + throw const DavException( + kind: DavErrorKind.network, + code: 'ConnectionDroppedAfterWrite', + safeMessage: 'Connection dropped.', + ); + }, + fetcher: (href, uri) async => _live(href, uri, '"server"', intended), + ); + final service = DavConditionalMutationService(remoteClient: remote); + + final result = await service.create( + collectionUri: _collectionUri, + object: DavNewObject( + uid: 'event@example.test', + initialMemberName: 'new.ics', + rawIcs: intended, + componentType: 'VEVENT', + ), + capabilities: _writable, + correlationId: 'unknown-create', + ); + + expect(result.outcome, DavMutationOutcome.succeeded); + expect(puts, 1); + }, + ); + + test( + '412 update auto-merges disjoint properties and retries exact current ETag', + () async { + final baseline = _event('Baseline', location: 'One'); + final remoteBody = _event('Baseline', location: 'Remote room'); + final calls = <({String? ifMatch, String raw})>[]; + var fetches = 0; + late String merged; + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + calls.add((ifMatch: ifMatch, raw: rawIcs)); + if (calls.length == 1) return _precondition; + merged = rawIcs; + return _success; + }, + fetcher: (href, uri) async { + fetches += 1; + return fetches == 1 + ? _live(href, uri, '"remote-etag"', remoteBody) + : _live(href, uri, '"canonical"', merged); + }, + ); + final patch = DavMutationPatch( + target: _target, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'Local title')], + ); + + final result = + await DavConditionalMutationService( + remoteClient: remote, + nowUtc: () => DateTime.utc(2026, 8, 8), + ).update( + hrefKey: _href, + uri: _uri, + baselineEtag: '"baseline-etag"', + baselineRawIcs: baseline, + patch: patch, + capabilities: _writable, + correlationId: 'merge', + ); + + expect(result.outcome, DavMutationOutcome.succeeded); + expect(calls.map((call) => call.ifMatch), [ + '"baseline-etag"', + '"remote-etag"', + ]); + expect(calls.last.raw, contains('SUMMARY:Local title')); + expect(calls.last.raw, contains('LOCATION:Remote room')); + }, + ); + + test( + 'overlapping update creates conflict and does not overwrite remote', + () async { + final calls = []; + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + calls.add(ifMatch); + return _precondition; + }, + fetcher: (href, uri) async => + _live(href, uri, '"remote"', _event('Remote title')), + ); + final result = await DavConditionalMutationService(remoteClient: remote) + .update( + hrefKey: _href, + uri: _uri, + baselineEtag: '"baseline"', + baselineRawIcs: _event('Baseline'), + patch: DavMutationPatch( + target: _target, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'Local title')], + ), + capabilities: _writable, + correlationId: 'conflict', + ); + + expect(result.outcome, DavMutationOutcome.conflict); + expect(result.conflict?.conflictCode, 'DavConflictOverlappingProperties'); + expect(calls, ['"baseline"']); + }, + ); + + test('lost update response adopts matching server content', () async { + late String intended; + var puts = 0; + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + puts += 1; + intended = rawIcs; + throw const DavException( + kind: DavErrorKind.timeout, + code: 'DavResponseTimeout', + safeMessage: 'Timed out.', + ); + }, + fetcher: (href, uri) async => _live(href, uri, '"updated"', intended), + ); + + final result = await DavConditionalMutationService(remoteClient: remote) + .update( + hrefKey: _href, + uri: _uri, + baselineEtag: '"baseline"', + baselineRawIcs: _event('Baseline'), + patch: DavMutationPatch( + target: _target, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'Intended')], + ), + capabilities: _writable, + correlationId: 'unknown-update', + ); + + expect(result.outcome, DavMutationOutcome.succeeded); + expect(puts, 1); + }); + + test( + 'stale delete conflicts while unknown delete with 404 is completed', + () async { + final stale = _FakeMutationRemote( + delete: ({required uri, required ifMatch}) async => _precondition, + fetcher: (href, uri) async => + _live(href, uri, '"changed"', _event('Remote changed')), + ); + final conflict = await DavConditionalMutationService(remoteClient: stale) + .delete( + hrefKey: _href, + uri: _uri, + baselineEtag: '"baseline"', + baselineRawIcs: _event('Baseline'), + isEvent: true, + capabilities: _writable, + correlationId: 'stale-delete', + ); + expect(conflict.outcome, DavMutationOutcome.conflict); + expect(conflict.conflict?.conflictCode, 'DavConflictStaleDelete'); + + var deletes = 0; + final unknown = _FakeMutationRemote( + delete: ({required uri, required ifMatch}) async { + deletes += 1; + throw const DavException( + kind: DavErrorKind.network, + code: 'Dropped', + safeMessage: 'Dropped.', + ); + }, + fetcher: (href, uri) async => + DavFetchedMember.missing(hrefKey: href, requestUri: uri), + ); + final deleted = await DavConditionalMutationService(remoteClient: unknown) + .delete( + hrefKey: _href, + uri: _uri, + baselineEtag: '"baseline"', + baselineRawIcs: _event('Baseline'), + isEvent: true, + capabilities: _writable, + correlationId: 'unknown-delete', + ); + expect(deleted.outcome, DavMutationOutcome.succeeded); + expect(deletes, 1); + }, + ); + + test('MOVE preserves the resource and adopts the destination object', () async { + final targetUri = Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/home/event.ics', + ); + final destinationHref = targetUri.path; + var moves = 0; + final remote = _FakeMutationRemote( + move: + ({ + required sourceUri, + required destinationUri, + required ifMatch, + }) async { + moves += 1; + expect(sourceUri, _uri); + expect(destinationUri, targetUri); + expect(ifMatch, 'W/"baseline"'); + return _success; + }, + fetcher: (href, uri) async { + expect(href, destinationHref); + return _live(href, uri, '"moved"', _event('Baseline')); + }, + ); + + final result = await DavConditionalMutationService(remoteClient: remote) + .move( + sourceHrefKey: _href, + sourceUri: _uri, + destinationHrefKey: destinationHref, + destinationUri: targetUri, + baselineEtag: 'W/"baseline"', + baselineRawIcs: _event('Baseline'), + isEvent: true, + sourceCapabilities: _writable, + destinationCapabilities: _writable, + correlationId: 'move', + ); + + expect(moves, 1); + expect(result.outcome, DavMutationOutcome.succeeded); + expect(result.canonicalObject?.hrefKey, destinationHref); + }); + + test( + 'lost MOVE response reconciles destination without repeating MOVE', + () async { + final destinationUri = Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/home/event.ics', + ); + final destinationHref = destinationUri.path; + var moves = 0; + final remote = _FakeMutationRemote( + move: + ({ + required sourceUri, + required destinationUri, + required ifMatch, + }) async { + moves += 1; + throw const DavException( + kind: DavErrorKind.network, + code: 'ConnectionDroppedAfterMove', + safeMessage: 'Connection dropped.', + ); + }, + fetcher: (href, uri) async => href == destinationHref + ? _live(href, uri, '"moved"', _event('Baseline')) + : DavFetchedMember.missing(hrefKey: href, requestUri: uri), + ); + + final result = await DavConditionalMutationService(remoteClient: remote) + .move( + sourceHrefKey: _href, + sourceUri: _uri, + destinationHrefKey: destinationHref, + destinationUri: destinationUri, + baselineEtag: 'W/"baseline"', + baselineRawIcs: _event('Baseline'), + isEvent: true, + sourceCapabilities: _writable, + destinationCapabilities: _writable, + correlationId: 'unknown-move', + ); + + expect(moves, 1); + expect(result.outcome, DavMutationOutcome.succeeded); + }, + ); + + test('read-only capability blocks mutation before any request', () async { + var requested = false; + final remote = _FakeMutationRemote( + put: + ({ + required uri, + required rawIcs, + required ifMatch, + required ifNoneMatch, + }) async { + requested = true; + return _success; + }, + ); + await expectLater( + DavConditionalMutationService(remoteClient: remote).update( + hrefKey: _href, + uri: _uri, + baselineEtag: '"etag"', + baselineRawIcs: _event('Baseline'), + patch: DavMutationPatch( + target: _target, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'No')], + ), + capabilities: const CollectionCapabilities(supportsEvents: true), + correlationId: 'read-only', + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavReadOnly', + ), + ), + ); + expect(requested, isFalse); + }); + + test( + 'HTTP mutation client maps UID conflicts and invalid calendar data', + () async { + final requests = []; + final profile = davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: Uri.parse('https://cloud.example.test'), + ); + final transport = DavHttpTransport( + client: MockClient((request) async { + requests.add(request); + if (request.url.path.endsWith('conflict.ics')) { + return http.Response( + '' + '', + 409, + ); + } + if (request.url.path.endsWith('invalid.ics')) { + return http.Response( + '' + 'UnsupportedMediaType' + '', + 415, + ); + } + return http.Response('', request.method == 'PUT' ? 201 : 204); + }), + profile: profile, + accountAuthority: Uri.parse('https://cloud.example.test'), + delay: (_) async {}, + random: Random(1), + ); + final client = DavMutationHttpClient( + transport: transport, + accountId: 'account', + collectionId: 'collection', + credential: DavBasicCredential(username: 'alex', password: 'secret'), + ); + + await client.conditionalPut( + uri: _uri, + rawIcs: _event('Create'), + correlationId: 'put', + ifNoneMatch: true, + ); + await client.conditionalDelete( + uri: _uri, + ifMatch: 'W/"opaque"', + correlationId: 'delete', + ); + final moveDestination = _collectionUri.resolve('moved.ics'); + await client.conditionalMove( + sourceUri: _uri, + destinationUri: moveDestination, + ifMatch: 'W/"move"', + correlationId: 'move', + ); + expect(requests[0].headers['if-none-match'], '*'); + expect(requests[0].headers, isNot(contains('if-match'))); + expect(requests[1].headers['if-match'], 'W/"opaque"'); + expect(requests[2].method, 'MOVE'); + expect(requests[2].headers['destination'], moveDestination.toString()); + expect(requests[2].headers['depth'], 'infinity'); + expect(requests[2].headers['overwrite'], 'F'); + expect(requests[2].headers['if-match'], 'W/"move"'); + + await expectLater( + client.conditionalPut( + uri: _collectionUri.resolve('conflict.ics'), + rawIcs: _event('Conflict'), + correlationId: 'uid', + ifNoneMatch: true, + ), + throwsA( + isA() + .having((error) => error.kind, 'kind', DavErrorKind.uidConflict) + .having((error) => error.code, 'code', 'DavUidConflict'), + ), + ); + await expectLater( + client.conditionalPut( + uri: _collectionUri.resolve('invalid.ics'), + rawIcs: _event('Invalid'), + correlationId: 'invalid', + ifNoneMatch: true, + ), + throwsA( + isA() + .having( + (error) => error.kind, + 'kind', + DavErrorKind.invalidCalendarData, + ) + .having((error) => error.code, 'code', 'DavMalformedResource') + .having((error) => error.statusCode, 'statusCode', 415), + ), + ); + }, + ); +} + +typedef _PutCallback = + Future Function({ + required Uri uri, + required String rawIcs, + required String? ifMatch, + required bool ifNoneMatch, + }); +typedef _DeleteCallback = + Future Function({ + required Uri uri, + required String ifMatch, + }); +typedef _MoveCallback = + Future Function({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + }); +typedef _FetchCallback = + Future Function(String href, Uri uri); + +final class _FakeMutationRemote implements DavMutationRemoteClient { + _FakeMutationRemote({this.put, this.delete, this.move, this.fetcher}); + + final _PutCallback? put; + final _DeleteCallback? delete; + final _MoveCallback? move; + final _FetchCallback? fetcher; + + @override + Future conditionalDelete({ + required Uri uri, + required String ifMatch, + required String correlationId, + }) => delete!(uri: uri, ifMatch: ifMatch); + + @override + Future conditionalPut({ + required Uri uri, + required String rawIcs, + required String correlationId, + String? ifMatch, + bool ifNoneMatch = false, + }) => put!( + uri: uri, + rawIcs: rawIcs, + ifMatch: ifMatch, + ifNoneMatch: ifNoneMatch, + ); + + @override + Future conditionalMove({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + required String correlationId, + }) => move!( + sourceUri: sourceUri, + destinationUri: destinationUri, + ifMatch: ifMatch, + ); + + @override + Future fetch({ + required String hrefKey, + required Uri uri, + required String correlationId, + }) => fetcher!(hrefKey, uri); +} + +const _success = DavConditionalResponse( + status: DavConditionalStatus.success, + statusCode: 204, + etag: null, +); +const _precondition = DavConditionalResponse( + status: DavConditionalStatus.preconditionFailed, + statusCode: 412, + etag: null, +); + +const _writable = CollectionCapabilities( + canWriteContent: true, + canAddMembers: true, + canDeleteMembers: true, + supportsEvents: true, + supportsTasks: true, +); +const _target = IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', +); +final _collectionUri = Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/work/', +); +const _href = '/remote.php/dav/calendars/alex/work/event.ics'; +final _uri = Uri.parse('https://cloud.example.test$_href'); + +DavFetchedMember _live(String href, Uri uri, String etag, String body) => + DavFetchedMember.live( + hrefKey: href, + requestUri: uri, + etag: etag, + contentType: 'text/calendar', + rawIcsBody: body, + ); + +String _event(String summary, {String location = 'One'}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:event@example.test\r +DTSTART:20260808T090000Z\r +DTEND:20260808T100000Z\r +SUMMARY:$summary\r +LOCATION:$location\r +X-UNKNOWN:keep\r +END:VEVENT\r +END:VCALENDAR\r +'''; diff --git a/test/dav/mutation/dav_mutation_patch_test.dart b/test/dav/mutation/dav_mutation_patch_test.dart new file mode 100644 index 0000000..63d0bd1 --- /dev/null +++ b/test/dav/mutation/dav_mutation_patch_test.dart @@ -0,0 +1,684 @@ +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/ical/ical_semantics.dart'; +import 'package:busymax/src/dav/mutation/dav_conflict_analyzer.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/mutation/dav_projection_mutations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const target = IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + ); + + test( + 'patch codec is versioned and narrow edits preserve unknown content', + () { + final patch = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'Updated, title'), + DavPatchOperation.replaceRepeatedRaw('CATEGORIES', const [ + DavRawPropertyValue(value: r'One\, retained'), + DavRawPropertyValue(value: 'Two'), + ]), + ], + ); + final decoded = DavMutationPatch.fromJsonString(patch.toJsonString()); + final result = decoded.applyTo( + _baselineEvent, + nowUtc: DateTime.utc(2026, 8, 8, 12), + ); + final semantic = IcalSemanticDocument.parse(result); + final master = semantic.components.first; + + expect(decoded.schemaVersion, davMutationPatchSchemaVersion); + expect(master.summary, 'Updated, title'); + expect(master.categories, ['One, retained', 'Two']); + expect(result, contains('X-UNKNOWN;X-PARAM="a,b":keep-me')); + expect(result, contains('BEGIN:VTIMEZONE')); + expect('BEGIN:VALARM'.allMatches(result), hasLength(2)); + expect(result, contains('RECURRENCE-ID;TZID=America/Vancouver')); + }, + ); + + test( + 'task progress patch writes coherent complete, reopen, and partial states', + () { + const taskTarget = IcalComponentKey( + componentType: 'VTODO', + uid: 'task@example.test', + ); + String apply(int percent) => DavMutationPatch( + target: taskTarget, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setTaskProgress(percent)], + ).applyTo(_task, nowUtc: DateTime.utc(2026, 8, 8, 12, 34, 56)); + + final completed = IcalSemanticDocument.parse( + apply(100), + ).components.single; + expect(completed.status, 'COMPLETED'); + expect(completed.percentComplete, 100); + expect(completed.completed?.rawValue, '20260808T123456Z'); + + final reopened = IcalSemanticDocument.parse(apply(0)).components.single; + expect(reopened.status, 'NEEDS-ACTION'); + expect(reopened.percentComplete, isNull); + expect(reopened.completed, isNull); + + final partial = IcalSemanticDocument.parse(apply(55)).components.single; + expect(partial.status, 'IN-PROCESS'); + expect(partial.percentComplete, 55); + expect(partial.completed, isNull); + }, + ); + + test('parent patch preserves non-parent relationships', () { + const taskTarget = IcalComponentKey( + componentType: 'VTODO', + uid: 'task@example.test', + ); + final result = DavMutationPatch( + target: taskTarget, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setTaskParent('new-parent')], + ).applyTo(_task, nowUtc: DateTime.utc(2026, 8, 8)); + final component = IcalSemanticDocument.parse(result).components.single; + + expect(component.parentUid, 'new-parent'); + expect(result, contains('RELATED-TO;RELTYPE=CHILD:child-uid')); + expect(result, contains('RELATED-TO;RELTYPE=SIBLING:sibling-uid')); + expect(result, isNot(contains('RELATED-TO:old-parent'))); + }); + + test('editing one alarm preserves unsupported sibling alarms', () { + final replacement = IcalComponent( + name: 'VALARM', + children: [ + _property('ACTION', 'DISPLAY'), + _property('TRIGGER', '-PT45M'), + _property('DESCRIPTION', 'Changed'), + ], + originalBeginLine: 'BEGIN:VALARM', + originalEndLine: 'END:VALARM', + structurallyDirty: true, + ); + final result = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.replaceAlarm(alarmIndex: 0, alarm: replacement), + ], + ).applyTo(_baselineEvent, nowUtc: DateTime.utc(2026, 8, 8)); + + expect('BEGIN:VALARM'.allMatches(result), hasLength(2)); + expect(result, contains('TRIGGER:-PT45M')); + expect(result, contains('ACTION:AUDIO')); + expect(result, contains('X-ALARM-UNKNOWN:keep')); + }); + + test('UTC task dates preserve the entered wall-clock value', () { + final object = buildDavTaskObject( + const { + 'title': 'UTC task', + 'microsoftDueDateTime': { + 'dateTime': '2026-08-09T09:30:00', + 'timeZone': 'UTC', + }, + 'microsoftDueTimeZone': 'UTC', + }, + idFactory: () => 'utc-task', + nowUtc: () => DateTime.utc(2026, 8, 8), + ); + + final task = IcalSemanticDocument.parse(object.rawIcs).components.single; + expect(task.due?.rawValue, '20260809T093000Z'); + expect(task.due?.kind, IcalTemporalKind.utcDateTime); + }); + + test('task category edits retain multiple property parameters', () { + const baseline = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +BEGIN:VTODO\r +UID:categories@example.test\r +SUMMARY:Categories\r +CATEGORIES;LANGUAGE=en:One,Two\r +CATEGORIES;X-KEEP=yes:Three\r +END:VTODO\r +END:VCALENDAR\r +'''; + final patch = buildDavTaskUpdatePatch( + target: const IcalComponentKey( + componentType: 'VTODO', + uid: 'categories@example.test', + ), + baselineRawIcs: baseline, + fields: const { + 'categories': ['Two', 'Three', 'Four'], + }, + ); + + final result = patch!.applyTo(baseline, nowUtc: DateTime.utc(2026, 8, 8)); + expect(result, contains('CATEGORIES;LANGUAGE=en:Two,Four')); + expect(result, contains('CATEGORIES;X-KEEP=yes:Three')); + expect(IcalSemanticDocument.parse(result).components.single.categories, [ + 'Two', + 'Four', + 'Three', + ]); + }); + + test('task reminder update replaces DISPLAY and preserves AUDIO sibling', () { + const baseline = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +BEGIN:VTODO\r +UID:task-alarm@example.test\r +DTSTART:20260809T090000Z\r +DUE:20260809T100000Z\r +SUMMARY:Task with alarms\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +DESCRIPTION:Imported reminder\r +TRIGGER;VALUE=DATE-TIME:20260809T080000Z\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +ATTACH:Glass\r +X-ALARM-KEEP:opaque\r +END:VALARM\r +END:VTODO\r +END:VCALENDAR\r +'''; + final patch = buildDavTaskUpdatePatch( + target: const IcalComponentKey( + componentType: 'VTODO', + uid: 'task-alarm@example.test', + ), + baselineRawIcs: baseline, + fields: const { + 'microsoftIsReminderOn': true, + 'microsoftReminderDateTime': { + 'dateTime': '2026-08-09T07:30:00Z', + 'timeZone': 'UTC', + }, + }, + ); + + final result = patch!.applyTo(baseline, nowUtc: DateTime.utc(2026, 8, 8)); + expect('BEGIN:VALARM'.allMatches(result), hasLength(2)); + expect(result, contains('TRIGGER;VALUE=DATE-TIME:20260809T073000Z')); + expect(result, isNot(contains('20260809T080000Z'))); + expect(result, contains('ACTION:AUDIO')); + expect(result, contains('ATTACH:Glass')); + expect(result, contains('X-ALARM-KEEP:opaque')); + }); + + test( + 'recurring task completion records an exception and advances the master', + () { + final patch = buildDavRecurringTaskCompletionPatch( + target: const IcalComponentKey( + componentType: 'VTODO', + uid: 'recurring-task@example.test', + ), + baselineRawIcs: _recurringTask, + completedAtUtc: DateTime.utc(2026, 8, 9, 17, 30), + nowUtc: () => DateTime.utc(2026, 8, 9, 18), + ); + final result = patch.applyTo( + _recurringTask, + nowUtc: DateTime.utc(2026, 8, 9, 18), + ); + final semantic = IcalSemanticDocument.parse(result); + final master = semantic.components.singleWhere( + (component) => component.recurrenceId == null, + ); + final exception = semantic.components.singleWhere( + (component) => component.recurrenceId != null, + ); + + expect(master.start?.rawValue, '20260810'); + expect(master.due?.rawValue, '20260811'); + expect(master.recurrenceRules, ['FREQ=DAILY;COUNT=2']); + expect(master.status, isNull); + expect(master.percentComplete, isNull); + expect(master.completed, isNull); + expect(master.lastModified?.rawValue, '20260809T180000Z'); + expect(master.dtstamp?.rawValue, '20260809T180000Z'); + + expect(exception.recurrenceIdKey, 'VALUE=DATE:20260810'); + expect(exception.start?.rawValue, '20260809'); + expect(exception.due?.rawValue, '20260810'); + expect(exception.summary, 'Recurring task'); + expect(exception.description, 'Retained details'); + expect(exception.location, 'Vancouver'); + expect(exception.url, 'https://cloud.example.test/task/1'); + expect(exception.priority, 4); + expect(exception.classification, 'PUBLIC'); + expect(exception.status, 'COMPLETED'); + expect(exception.percentComplete, 100); + expect(exception.completed?.rawValue, '20260809T173000Z'); + expect(result, contains('X-UNKNOWN:keep-me')); + expect(result, contains('BEGIN:VALARM')); + }, + ); + + test('last COUNT occurrence completes both exception and master', () { + final baseline = _recurringTask.replaceFirst('COUNT=3', 'COUNT=1'); + final result = buildDavRecurringTaskCompletionPatch( + target: const IcalComponentKey( + componentType: 'VTODO', + uid: 'recurring-task@example.test', + ), + baselineRawIcs: baseline, + nowUtc: () => DateTime.utc(2026, 8, 9, 18), + ).applyTo(baseline, nowUtc: DateTime.utc(2026, 8, 9, 18)); + final components = IcalSemanticDocument.parse(result).components; + final master = components.singleWhere( + (component) => component.recurrenceId == null, + ); + + expect(components, hasLength(2)); + expect(master.due?.rawValue, '20260810'); + expect(master.recurrenceRules, ['FREQ=DAILY;COUNT=1']); + expect(master.status, 'COMPLETED'); + expect(master.percentComplete, 100); + expect(master.completed?.rawValue, '20260809T180000Z'); + }); + + test('an exhausted UNTIL rule retains the master and records history', () { + final baseline = _recurringTask.replaceFirst( + 'FREQ=DAILY;COUNT=3', + 'FREQ=DAILY;UNTIL=20260810', + ); + final result = buildDavRecurringTaskCompletionPatch( + target: const IcalComponentKey( + componentType: 'VTODO', + uid: 'recurring-task@example.test', + ), + baselineRawIcs: baseline, + nowUtc: () => DateTime.utc(2026, 8, 9, 18), + ).applyTo(baseline, nowUtc: DateTime.utc(2026, 8, 9, 18)); + final components = IcalSemanticDocument.parse(result).components; + final master = components.singleWhere( + (component) => component.recurrenceId == null, + ); + + expect(components, hasLength(2)); + expect(master.due?.rawValue, '20260810'); + expect(master.start?.rawValue, '20260809'); + expect(master.status, isNull); + expect(master.recurrenceRules, ['FREQ=DAILY;UNTIL=20260810']); + }); + + test( + 'component add and remove round-trip without replacing the resource', + () { + final exception = IcalComponent( + name: 'VEVENT', + children: [ + _property('UID', 'event@example.test'), + _property('DTSTAMP', '20260808T120000Z'), + _property('RECURRENCE-ID', '20260817T090000'), + _property('DTSTART', '20260817T130000'), + _property('DTEND', '20260817T140000'), + _property('SUMMARY', 'Added exception'), + ], + originalBeginLine: 'BEGIN:VEVENT', + originalEndLine: 'END:VEVENT', + structurallyDirty: true, + ); + final add = DavMutationPatch( + target: const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + recurrenceIdKey: '20260817T090000', + ), + scope: DavMutationScope.occurrence, + operations: [DavPatchOperation.addComponent(exception)], + ); + final decoded = DavMutationPatch.fromJsonString(add.toJsonString()); + final added = decoded.applyTo( + _baselineEvent, + nowUtc: DateTime.utc(2026, 8, 8), + ); + + expect('BEGIN:VEVENT'.allMatches(added), hasLength(3)); + expect(added, contains('SUMMARY:Added exception')); + expect(added, contains('X-UNKNOWN;X-PARAM="a,b":keep-me')); + expect(decoded.changedProperties, {'COMPONENT-SET'}); + + final removed = DavMutationPatch( + target: const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + recurrenceIdKey: 'TZID=America/Vancouver:20260810T090000', + ), + scope: DavMutationScope.recurrenceException, + operations: [DavPatchOperation.removeComponent()], + ).applyTo(_baselineEvent, nowUtc: DateTime.utc(2026, 8, 8)); + expect('BEGIN:VEVENT'.allMatches(removed), hasLength(1)); + expect(removed, contains('BEGIN:VTIMEZONE')); + expect(removed, contains('X-UNKNOWN;X-PARAM="a,b":keep-me')); + }, + ); + + test('component-set mutation conflicts with any semantic remote edit', () { + final patch = DavMutationPatch( + target: const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + recurrenceIdKey: '20260817T090000', + ), + scope: DavMutationScope.occurrence, + operations: [ + DavPatchOperation.addComponent( + IcalComponent( + name: 'VEVENT', + children: [ + _property('UID', 'event@example.test'), + _property('RECURRENCE-ID', '20260817T090000'), + _property('DTSTART', '20260817T090000'), + _property('DTEND', '20260817T100000'), + ], + originalBeginLine: 'BEGIN:VEVENT', + originalEndLine: 'END:VEVENT', + structurallyDirty: true, + ), + ), + ], + ); + + final result = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: _baselineEvent.replaceFirst( + 'LOCATION:Room one', + 'LOCATION:Remote room', + ), + localPatch: patch, + nowUtc: DateTime.utc(2026, 8, 8), + ); + expect(result.outcome, DavConflictOutcome.conflict); + expect(result.conflictCode, 'DavConflictBroadRecurrenceChange'); + }); + + test('one-occurrence edit adds an in-resource detached exception', () { + final patch = buildDavEventOccurrenceExceptionPatch( + uid: 'event@example.test', + occurrenceKey: 'TZID=America/Vancouver:20260817T090000', + baselineRawIcs: _baselineEvent, + input: DavEventMutationInput( + title: 'Only this occurrence', + allDay: false, + start: DateTime(2026, 8, 17, 14), + end: DateTime(2026, 8, 17, 15), + startTimeZone: 'America/Vancouver', + endTimeZone: 'America/Vancouver', + reminders: const { + 'overrides': [ + {'minutes': 15}, + {'minutes': 45}, + ], + }, + ), + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ); + final candidate = patch.applyTo( + _baselineEvent, + nowUtc: DateTime.utc(2026, 8, 8, 12), + ); + final semantic = IcalSemanticDocument.parse(candidate); + final added = semantic.components.singleWhere( + (component) => + component.recurrenceIdKey == 'TZID=America/Vancouver:20260817T090000', + ); + + expect(patch.scope, DavMutationScope.occurrence); + expect(added.summary, 'Only this occurrence'); + expect(added.start?.rawValue, '20260817T140000'); + expect(added.alarms, hasLength(2)); + expect(candidate, contains('RRULE:FREQ=WEEKLY;COUNT=2')); + expect(candidate, contains('X-UNKNOWN;X-PARAM="a,b":keep-me')); + }); + + test('one-occurrence delete creates or updates a cancelled exception', () { + final generated = buildDavEventOccurrenceCancellationPatch( + uid: 'event@example.test', + occurrenceKey: 'TZID=America/Vancouver:20260817T090000', + baselineRawIcs: _baselineEvent, + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + ).applyTo(_baselineEvent, nowUtc: DateTime.utc(2026, 8, 8, 12)); + expect(generated, contains('STATUS:CANCELLED')); + expect('BEGIN:VEVENT'.allMatches(generated), hasLength(3)); + + final existing = buildDavEventOccurrenceCancellationPatch( + uid: 'event@example.test', + occurrenceKey: 'TZID=America/Vancouver:20260810T090000', + baselineRawIcs: _baselineEvent, + ).applyTo(_baselineEvent, nowUtc: DateTime.utc(2026, 8, 8, 12)); + final exception = IcalSemanticDocument.parse( + existing, + ).components.singleWhere((component) => component.recurrenceIdKey != null); + expect(exception.status, 'CANCELLED'); + expect('BEGIN:VEVENT'.allMatches(existing), hasLength(2)); + }); + + test( + 'three-way merge applies disjoint local change to current server body', + () { + final remote = _baselineEvent.replaceFirst( + 'LOCATION:Room one', + 'LOCATION:Room two', + ); + final patch = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [DavPatchOperation.setText('SUMMARY', 'Local summary')], + ); + + final result = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: remote, + localPatch: patch, + nowUtc: DateTime.utc(2026, 8, 8), + ); + + expect(result.outcome, DavConflictOutcome.autoMerged); + expect(result.remoteChangedProperties, {'LOCATION'}); + expect(result.mergedRawIcs, contains('SUMMARY:Local summary')); + expect(result.mergedRawIcs, contains('LOCATION:Room two')); + expect(result.mergedRawIcs, contains('X-UNKNOWN')); + }, + ); + + test( + 'same-property and broad recurrence edits become explicit conflicts', + () { + final summaryPatch = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [DavPatchOperation.setText('SUMMARY', 'Local')], + ); + final sameProperty = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: _baselineEvent.replaceFirst( + 'SUMMARY:Baseline', + 'SUMMARY:Remote', + ), + localPatch: summaryPatch, + nowUtc: DateTime.utc(2026, 8, 8), + ); + expect(sameProperty.outcome, DavConflictOutcome.conflict); + expect(sameProperty.conflictCode, 'DavConflictOverlappingProperties'); + + final recurrencePatch = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [DavPatchOperation.setRaw('RRULE', 'FREQ=DAILY;COUNT=5')], + ); + final broad = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: _baselineEvent.replaceFirst( + 'DESCRIPTION:Baseline details', + 'DESCRIPTION:Remote details', + ), + localPatch: recurrencePatch, + nowUtc: DateTime.utc(2026, 8, 8), + ); + expect(broad.outcome, DavConflictOutcome.conflict); + expect(broad.conflictCode, 'DavConflictBroadRecurrenceChange'); + }, + ); + + test( + 'exception-set change and stale delete cannot be silently overwritten', + () { + final local = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [DavPatchOperation.setText('SUMMARY', 'Local')], + ); + final addedException = _baselineEvent.replaceFirst( + 'END:VCALENDAR', + '''BEGIN:VEVENT\r +UID:event@example.test\r +RECURRENCE-ID;TZID=America/Vancouver:20260817T090000\r +DTSTART;TZID=America/Vancouver:20260817T120000\r +DTEND;TZID=America/Vancouver:20260817T130000\r +SUMMARY:New exception\r +END:VEVENT\r +END:VCALENDAR''', + ); + final recurrenceSet = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: addedException, + localPatch: local, + nowUtc: DateTime.utc(2026, 8, 8), + ); + expect(recurrenceSet.outcome, DavConflictOutcome.conflict); + expect(recurrenceSet.remoteChangedProperties, {'RECURRENCE-SET'}); + + final staleDelete = const DavConflictAnalyzer().analyzeDelete( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: _baselineEvent.replaceFirst( + 'LOCATION:Room one', + 'LOCATION:Remote room', + ), + ); + expect(staleDelete.outcome, DavConflictOutcome.conflict); + expect(staleDelete.conflictCode, 'DavConflictStaleDelete'); + }, + ); + + test('server folding canonicalization is not treated as a conflict', () { + final folded = _baselineEvent.replaceFirst( + 'DESCRIPTION:Baseline details', + 'DESCRIPTION:Baseline\r\n details', + ); + final patch = DavMutationPatch( + target: target, + scope: DavMutationScope.recurrenceMaster, + operations: [DavPatchOperation.setText('SUMMARY', 'Local')], + ); + final result = const DavConflictAnalyzer().analyzeUpdate( + baselineRawIcs: _baselineEvent, + currentRemoteRawIcs: folded, + localPatch: patch, + nowUtc: DateTime.utc(2026, 8, 8), + ); + + expect(result.outcome, DavConflictOutcome.remoteUnchanged); + expect(result.mergedRawIcs, contains('SUMMARY:Local')); + }); +} + +IcalProperty _property(String name, String value) => IcalProperty( + group: null, + name: name, + parameters: const [], + rawValue: value, + originalPhysicalLines: const [], + isDirty: true, +); + +const _baselineEvent = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VTIMEZONE\r +TZID:America/Vancouver\r +X-LIC-LOCATION:America/Vancouver\r +END:VTIMEZONE\r +BEGIN:VEVENT\r +UID:event@example.test\r +DTSTART;TZID=America/Vancouver:20260803T090000\r +DTEND;TZID=America/Vancouver:20260803T100000\r +RRULE:FREQ=WEEKLY;COUNT=2\r +SUMMARY:Baseline\r +DESCRIPTION:Baseline details\r +LOCATION:Room one\r +CATEGORIES:One\r +X-UNKNOWN;X-PARAM="a,b":keep-me\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT15M\r +DESCRIPTION:Reminder\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +X-ALARM-UNKNOWN:keep\r +END:VALARM\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:event@example.test\r +RECURRENCE-ID;TZID=America/Vancouver:20260810T090000\r +DTSTART;TZID=America/Vancouver:20260810T110000\r +DTEND;TZID=America/Vancouver:20260810T120000\r +SUMMARY:Moved\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +const _task = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//Nextcloud Tasks//EN\r +BEGIN:VTODO\r +UID:task@example.test\r +SUMMARY:Task\r +STATUS:IN-PROCESS\r +PERCENT-COMPLETE:25\r +COMPLETED:20260801T120000Z\r +RELATED-TO:old-parent\r +RELATED-TO;RELTYPE=CHILD:child-uid\r +RELATED-TO;RELTYPE=SIBLING:sibling-uid\r +X-PINNED:1\r +END:VTODO\r +END:VCALENDAR\r +'''; + +const _recurringTask = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//Nextcloud Tasks//EN\r +BEGIN:VTODO\r +UID:recurring-task@example.test\r +DTSTAMP:20260801T120000Z\r +DTSTART;VALUE=DATE:20260809\r +DUE;VALUE=DATE:20260810\r +RRULE:FREQ=DAILY;COUNT=3\r +SUMMARY:Recurring task\r +DESCRIPTION:Retained details\r +LOCATION:Vancouver\r +URL:https://cloud.example.test/task/1\r +PRIORITY:4\r +X-UNKNOWN:keep-me\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +DESCRIPTION:This is a todo reminder.\r +TRIGGER;RELATED=END:-PT1H\r +END:VALARM\r +END:VTODO\r +END:VCALENDAR\r +'''; diff --git a/test/dav/mutation/dav_pending_operations_test.dart b/test/dav/mutation/dav_pending_operations_test.dart new file mode 100644 index 0000000..0273f5a --- /dev/null +++ b/test/dav/mutation/dav_pending_operations_test.dart @@ -0,0 +1,798 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/mutation/dav_conditional_mutation_service.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/mutation/dav_pending_operations.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/dav/sync/dav_collection_remote_client.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late AppDatabase database; + late DavObjectRepository objectRepository; + late DavPendingOperationQueue queue; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + var objectId = 0; + objectRepository = DavObjectRepository( + database: database, + idFactory: () => 'raw-object-${objectId += 1}', + ); + await _seed(database, objectRepository); + queue = DavPendingOperationQueue( + database: database, + idFactory: () => 'pending-op', + nowUtc: () => _now, + ); + }); + + tearDown(() => database.close()); + + test( + 'queue retains exact baseline and safely coalesces unsent patches', + () async { + final object = await database.select(database.davObjects).getSingle(); + final first = await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'Local title'), + ); + final second = await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('LOCATION', 'Local room'), + ); + + expect(second, first); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.baselineEtag, 'W/"baseline"'); + expect(pending.baselineRawIcs, _event('Baseline')); + expect(pending.davCollectionHref, _collectionHref); + expect(pending.davMemberHref, _eventHref); + expect(pending.targetComponentKey, contains('event@example.test')); + expect(pending.mutationScope, 'object'); + expect(pending.requestJson, '{}'); + expect(pending.requestJson, isNot(contains('Authorization'))); + final decoded = DavMutationPatch.fromJsonString( + pending.mutationPatchJson!, + ); + expect(decoded.operations, hasLength(2)); + expect( + decoded.applyTo(pending.baselineRawIcs!, nowUtc: _now), + allOf(contains('SUMMARY:Local title'), contains('LOCATION:Local room')), + ); + }, + ); + + test( + 'a new replayer instance adopts canonical update after restart', + () async { + final object = await database.select(database.davObjects).getSingle(); + await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'After restart'), + ); + String? sentCandidate; + final remote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async { + expect(ifMatch, 'W/"baseline"'); + expect(ifNoneMatch, isFalse); + sentCandidate = rawIcs; + return _success; + }, + fetcher: (href) async => _live(href, '"canonical"', sentCandidate!), + ); + final notificationObjects = {}; + final followUpCollections = {}; + // Constructed after enqueue to model process/service reconstruction. + final replayer = DavPendingOperationsReplayer( + database: database, + accountId: 'account', + objectRepository: objectRepository, + serviceFactory: ({required account, required collection}) async => + DavConditionalMutationService(remoteClient: remote), + rebuildNotifications: (ids) async => notificationObjects.addAll(ids), + requestFollowUpSync: (ids) async => followUpCollections.addAll(ids), + idFactory: () => 'correlation', + nowUtc: () => _now, + random: Random(1), + ); + + final result = await replayer.replayDueOperations(); + + expect(result.appliedCount, 1); + expect(result.mutatedCollectionIds, {'collection'}); + expect(await database.select(database.pendingOps).get(), isEmpty); + final stored = await database.select(database.davObjects).getSingle(); + expect(stored.etag, '"canonical"'); + expect(stored.rawIcsBody, contains('SUMMARY:After restart')); + expect( + (await database.select(database.calendarEvents).getSingle()).title, + 'After restart', + ); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'token-1', + ); + expect(notificationObjects, {stored.id}); + expect(followUpCollections, {'collection'}); + }, + ); + + test('overlapping ETag edit persists all three conflict snapshots', () async { + final object = await database.select(database.davObjects).getSingle(); + await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'Local'), + ); + final remote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async => + _precondition, + fetcher: (href) async => _live(href, '"remote"', _event('Remote')), + ); + final replayer = _replayer(database, objectRepository, remote); + + final result = await replayer.replayDueOperations(); + + expect(result.conflictCount, 1); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'conflict'); + expect(pending.conflictState, 'unresolved'); + final snapshot = await database + .select(database.davConflictSnapshots) + .getSingle(); + expect(snapshot.baselineEtag, 'W/"baseline"'); + expect(snapshot.baselineRawIcs, _event('Baseline')); + expect(snapshot.localCandidateRawIcs, contains('SUMMARY:Local')); + expect(snapshot.remoteEtag, '"remote"'); + expect(snapshot.remoteRawIcs, _event('Remote')); + expect(snapshot.conflictCode, 'DavConflictOverlappingProperties'); + expect( + (await database.select(database.davObjects).getSingle()).rawIcsBody, + _event('Baseline'), + ); + }); + + test('revoked credential pauses replay and preserves pending work', () async { + final object = await database.select(database.davObjects).getSingle(); + await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'Keep locally'), + ); + final remote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async { + throw const DavException( + kind: DavErrorKind.authentication, + code: 'DavCredentialsRevoked', + safeMessage: 'The DAV credential was rejected.', + ); + }, + ); + + final result = await _replayer( + database, + objectRepository, + remote, + ).replayDueOperations(); + + expect(result.paused, isTrue); + final account = await database.select(database.accounts).getSingle(); + expect(account.authState, 'reauth_required'); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'auth_blocked'); + expect(pending.baselineRawIcs, _event('Baseline')); + expect(await database.select(database.davObjects).get(), hasLength(1)); + }); + + test('permission change blocks mutation before a network write', () async { + final object = await database.select(database.davObjects).getSingle(); + await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'No longer allowed'), + ); + await database + .update(database.davCollections) + .write(const DavCollectionsCompanion(readOnly: Value(true))); + var writes = 0; + final remote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async { + writes += 1; + return _success; + }, + ); + + final result = await _replayer( + database, + objectRepository, + remote, + ).replayDueOperations(); + + expect(result.paused, isTrue); + expect(writes, 0); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'permission_changed', + ); + expect( + (await database.select(database.pendingOps).getSingle()).state, + 'permission_blocked', + ); + }); + + test('permanent replay failure is reported after it is stored', () async { + final object = await database.select(database.davObjects).getSingle(); + await queue.enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: object.id, + patch: _patch('SUMMARY', 'Rejected'), + ); + final reported = []; + final remote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async { + throw const DavException( + kind: DavErrorKind.invalidCalendarData, + code: 'DavMalformedResource', + safeMessage: 'The DAV server rejected the calendar data.', + statusCode: 415, + ); + }, + ); + final replayer = DavPendingOperationsReplayer( + database: database, + accountId: 'account', + objectRepository: objectRepository, + serviceFactory: ({required account, required collection}) async => + DavConditionalMutationService(remoteClient: remote), + onPermanentFailure: (operation, error) async => reported.add(error), + idFactory: () => 'failure-correlation', + nowUtc: () => _now, + ); + + await replayer.replayDueOperations(); + + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'failed'); + expect(pending.lastErrorCode, 'DavMalformedResource'); + expect(reported, hasLength(1)); + expect(reported.single.statusCode, 415); + }); + + test('queue rejects a VTODO due before its start', () async { + await expectLater( + queue.enqueueCreate( + accountId: 'account', + collectionId: 'collection', + object: DavNewObject( + uid: 'invalid-task@example.test', + initialMemberName: 'invalid-task.ics', + rawIcs: _task( + uid: 'invalid-task@example.test', + start: 'DTSTART;VALUE=DATE:20260810', + due: 'DUE;VALUE=DATE:20260809', + ), + componentType: 'VTODO', + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavTaskDueBeforeStart', + ), + ), + ); + + expect(await database.select(database.pendingOps).get(), isEmpty); + }); + + test('queue rejects mixed all-day and timed VTODO ranges', () async { + await expectLater( + queue.enqueueCreate( + accountId: 'account', + collectionId: 'collection', + object: DavNewObject( + uid: 'mixed-task@example.test', + initialMemberName: 'mixed-task.ics', + rawIcs: _task( + uid: 'mixed-task@example.test', + start: 'DTSTART;TZID=America/Vancouver:20260810T090000', + due: 'DUE;VALUE=DATE:20260810', + ), + componentType: 'VTODO', + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavTaskTemporalTypeMismatch', + ), + ), + ); + }); + + test('an explicitly rejected create can be corrected and requeued', () async { + const projectionId = 'local-task'; + const uid = 'rejected-task@example.test'; + await queue.enqueueCreate( + accountId: 'account', + collectionId: 'collection', + localProjectionId: projectionId, + object: DavNewObject( + uid: uid, + initialMemberName: 'rejected-task.ics', + rawIcs: _task( + uid: uid, + start: 'DTSTART;VALUE=DATE:20260809', + due: 'DUE;VALUE=DATE:20260810', + ), + componentType: 'VTODO', + ), + ); + await database + .update(database.pendingOps) + .write( + const PendingOpsCompanion( + state: Value('failed'), + retryClassification: Value('permanent'), + nextAttemptAtUtc: Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: Value('DavMalformedResource'), + lastErrorMessage: Value( + 'The DAV server could not update the object.', + ), + ), + ); + + final updated = await queue.updateUnsentCreate( + accountId: 'account', + collectionId: 'collection', + localProjectionId: projectionId, + patch: DavMutationPatch( + target: const IcalComponentKey(componentType: 'VTODO', uid: uid), + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'Corrected task')], + ), + ); + + expect(updated, isTrue); + final pending = await database.select(database.pendingOps).getSingle(); + expect(pending.state, 'pending'); + expect(pending.retryClassification, 'conditional_create'); + expect(pending.nextAttemptAtUtc, isNull); + expect(pending.lastErrorCode, isNull); + expect(pending.lastErrorMessage, isNull); + expect(pending.requestJson, contains('SUMMARY:Corrected task')); + }); + + test( + 'MOVE replays to the same filename and commits destination projection', + () async { + await _seedDestination(database); + final source = await (database.select( + database.davObjects, + )..where((row) => row.hrefKey.equals(_eventHref))).getSingle(); + + await queue.enqueueMove( + accountId: 'account', + sourceCollectionId: 'collection', + destinationCollectionId: 'destination', + objectId: source.id, + target: _target, + ); + + final operation = await database.select(database.pendingOps).getSingle(); + const destinationHref = '/remote.php/dav/calendars/alex/home/event.ics'; + expect(operation.operationType, 'dav.move'); + expect(operation.destinationCollectionId, 'destination'); + expect(operation.destinationMemberHref, destinationHref); + expect(operation.requestJson, contains('event.ics')); + var moves = 0; + final remote = _FakeMutationRemote( + move: + ({ + required sourceUri, + required destinationUri, + required ifMatch, + }) async { + moves += 1; + expect(sourceUri.path, _eventHref); + expect(destinationUri.path, destinationHref); + expect(ifMatch, 'W/"baseline"'); + return _success; + }, + fetcher: (href) async { + expect(href, destinationHref); + return _live(href, '"moved"', _event('Baseline')); + }, + ); + + final result = await _replayer( + database, + objectRepository, + remote, + ).replayDueOperations(); + + expect(moves, 1); + expect(result.appliedCount, 1); + expect(result.mutatedCollectionIds, {'collection', 'destination'}); + expect(await database.select(database.pendingOps).get(), isEmpty); + final objects = await database.select(database.davObjects).get(); + expect( + objects.singleWhere((object) => object.id == source.id).serverDeleted, + isTrue, + ); + final moved = objects.singleWhere( + (object) => object.collectionId == 'destination', + ); + expect(moved.hrefKey, destinationHref); + expect(moved.serverDeleted, isFalse); + final event = await database.select(database.calendarEvents).getSingle(); + expect(event.calendarSourceId, 'dav-calendar-destination'); + expect(event.davCollectionId, 'destination'); + }, + ); + + test('conditional create and delete use confirmed server state', () async { + final createdBody = _eventWithUid('Created', 'created@example.test'); + await queue.enqueueCreate( + accountId: 'account', + collectionId: 'collection', + object: DavNewObject( + uid: 'created@example.test', + initialMemberName: 'opaque-file.ics', + rawIcs: createdBody, + componentType: 'VEVENT', + ), + ); + final createRemote = _FakeMutationRemote( + put: ({required rawIcs, required ifMatch, required ifNoneMatch}) async { + expect(ifNoneMatch, isTrue); + expect(ifMatch, isNull); + return _success; + }, + fetcher: (href) async => _live(href, '"created"', createdBody), + ); + await _replayer( + database, + objectRepository, + createRemote, + ).replayDueOperations(); + expect(await database.select(database.davObjects).get(), hasLength(2)); + expect(await database.select(database.pendingOps).get(), isEmpty); + + final baseline = await (database.select( + database.davObjects, + )..where((row) => row.hrefKey.equals(_eventHref))).getSingle(); + await queue.enqueueDelete( + accountId: 'account', + collectionId: 'collection', + objectId: baseline.id, + target: _target, + ); + var deletes = 0; + final deleteRemote = _FakeMutationRemote( + delete: ({required ifMatch}) async { + deletes += 1; + expect(ifMatch, 'W/"baseline"'); + return _success; + }, + ); + await _replayer( + database, + objectRepository, + deleteRemote, + ).replayDueOperations(); + + expect(deletes, 1); + expect( + (await (database.select( + database.davObjects, + )..where((row) => row.id.equals(baseline.id))).getSingle()).serverDeleted, + isTrue, + ); + }); +} + +DavPendingOperationsReplayer _replayer( + AppDatabase database, + DavObjectRepository repository, + DavMutationRemoteClient remote, +) => DavPendingOperationsReplayer( + database: database, + accountId: 'account', + objectRepository: repository, + serviceFactory: ({required account, required collection}) async => + DavConditionalMutationService(remoteClient: remote), + idFactory: () => 'generated-conflict-id', + nowUtc: () => _now, + random: Random(1), +); + +typedef _Put = + Future Function({ + required String rawIcs, + required String? ifMatch, + required bool ifNoneMatch, + }); +typedef _Delete = + Future Function({required String ifMatch}); +typedef _Move = + Future Function({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + }); +typedef _Fetch = Future Function(String href); + +final class _FakeMutationRemote implements DavMutationRemoteClient { + const _FakeMutationRemote({this.put, this.delete, this.move, this.fetcher}); + + final _Put? put; + final _Delete? delete; + final _Move? move; + final _Fetch? fetcher; + + @override + Future conditionalPut({ + required Uri uri, + required String rawIcs, + required String correlationId, + String? ifMatch, + bool ifNoneMatch = false, + }) => put!(rawIcs: rawIcs, ifMatch: ifMatch, ifNoneMatch: ifNoneMatch); + + @override + Future conditionalDelete({ + required Uri uri, + required String ifMatch, + required String correlationId, + }) => delete!(ifMatch: ifMatch); + + @override + Future conditionalMove({ + required Uri sourceUri, + required Uri destinationUri, + required String ifMatch, + required String correlationId, + }) => move!( + sourceUri: sourceUri, + destinationUri: destinationUri, + ifMatch: ifMatch, + ); + + @override + Future fetch({ + required String hrefKey, + required Uri uri, + required String correlationId, + }) => fetcher!(hrefKey); +} + +const _success = DavConditionalResponse( + status: DavConditionalStatus.success, + statusCode: 204, + etag: null, +); +const _precondition = DavConditionalResponse( + status: DavConditionalStatus.preconditionFailed, + statusCode: 412, + etag: null, +); + +DavFetchedMember _live(String href, String etag, String body) => + DavFetchedMember.live( + hrefKey: href, + requestUri: Uri.parse('https://cloud.example.test$href'), + etag: etag, + contentType: 'text/calendar', + rawIcsBody: body, + ); + +DavMutationPatch _patch(String property, String value) => DavMutationPatch( + target: _target, + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText(property, value)], +); + +const _target = IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', +); +const _collectionHref = '/remote.php/dav/calendars/alex/work/'; +const _eventHref = '${_collectionHref}event.ics'; +final _now = DateTime.utc(2026, 8, 8, 12); + +Future _seed(AppDatabase database, DavObjectRepository repository) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: _collectionHref, + requestUri: 'https://cloud.example.test$_collectionHref', + displayName: 'Work', + supportedComponentMask: const Value(3), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-collection', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: _collectionHref, + davCollectionId: const Value('collection'), + summary: 'Work', + createdAtLocal: _now.millisecondsSinceEpoch, + updatedAtLocal: _now.millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-collection', + davCollectionId: const Value('collection'), + title: 'Work', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); + await repository.commit( + DavCollectionCommit( + accountId: 'account', + collectionId: 'collection', + provider: BusyProvider.nextcloud, + objects: [ + DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: 'W/"baseline"', + contentType: 'text/calendar', + rawIcsBody: _event('Baseline'), + ), + ], + deletedHrefKeys: const {}, + completeMembership: true, + membershipHrefKeys: const {_eventHref}, + finalCursorKind: 'dav_sync_token', + finalCursorValue: 'token-1', + baselineGeneration: 1, + completedAtUtc: _now, + projectionRangeStartUtc: DateTime.utc(2025), + projectionRangeEndUtc: DateTime.utc(2029), + ), + ); +} + +Future _seedDestination(AppDatabase database) async { + const now = '2026-08-08T12:00:00.000Z'; + const href = '/remote.php/dav/calendars/alex/home/'; + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'destination', + accountId: 'account', + hrefKey: href, + requestUri: 'https://cloud.example.test$href', + displayName: 'Home', + supportedComponentMask: const Value(3), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-destination', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: href, + davCollectionId: const Value('destination'), + summary: 'Home', + createdAtLocal: _now.millisecondsSinceEpoch, + updatedAtLocal: _now.millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-destination', + davCollectionId: const Value('destination'), + title: 'Home', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} + +String _event(String summary) => _eventWithUid(summary, 'event@example.test'); + +String _task({ + required String uid, + required String start, + required String due, +}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VTODO\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +$start\r +$due\r +SUMMARY:Task\r +END:VTODO\r +END:VCALENDAR\r +'''; + +String _eventWithUid(String summary, String uid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTART:20260808T090000Z\r +DTEND:20260808T100000Z\r +SUMMARY:$summary\r +LOCATION:Baseline room\r +END:VEVENT\r +END:VCALENDAR\r +'''; diff --git a/test/dav/mutation/dav_repository_mutation_integration_test.dart b/test/dav/mutation/dav_repository_mutation_integration_test.dart new file mode 100644 index 0000000..3f13aba --- /dev/null +++ b/test/dav/mutation/dav_repository_mutation_integration_test.dart @@ -0,0 +1,1305 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/ical/ical_semantics.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; +import 'package:busymax/src/features/calendar/presentation/event_editor_draft.dart'; +import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late AppDatabase database; + late DavObjectRepository objectRepository; + late CalendarRepository calendarRepository; + late TasksRepository tasksRepository; + late int notificationChanges; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + var objectSequence = 0; + objectRepository = DavObjectRepository( + database: database, + idFactory: () => 'dav-object-${objectSequence += 1}', + ); + notificationChanges = 0; + calendarRepository = CalendarRepository( + database: database, + now: () => _now, + localTimeZone: 'America/Vancouver', + onNotificationScheduleChanged: () async => notificationChanges += 1, + ); + tasksRepository = TasksRepository( + database: database, + accountId: _accountId, + nowUtc: () => _now, + onNotificationScheduleChanged: () async => notificationChanges += 1, + ); + await _seedDavAccount(database); + }); + + tearDown(() => database.close()); + + test( + 'event create, unsent update, and delete remain one local unit', + () async { + await calendarRepository.createLocalEvent( + _newEventDraft().copyWith( + title: 'Local event', + description: 'Initial notes', + categories: const ['Work'], + ), + ); + + var events = await database.select(database.calendarEvents).get(); + var operations = await database.select(database.pendingOps).get(); + expect(events, hasLength(1)); + expect(events.single.id, startsWith('dav-local-event-')); + expect(events.single.davCollectionId, _collectionId); + expect(events.single.davObjectId, isNull); + expect(events.single.syncStatus, 'pending'); + expect(operations, hasLength(1)); + expect(operations.single.operationType, 'dav.create'); + expect(operations.single.eventId, events.single.id); + expect(_createRaw(operations.single), contains('SUMMARY:Local event')); + expect(_createRaw(operations.single), contains('CATEGORIES:Work')); + + await calendarRepository.updateLocalEvent( + _draftForEvent( + events.single, + ).copyWith(title: 'Edited before upload', location: 'Room 4'), + ); + + events = await database.select(database.calendarEvents).get(); + operations = await database.select(database.pendingOps).get(); + expect(events.single.title, 'Edited before upload'); + expect(events.single.location, 'Room 4'); + expect(operations, hasLength(1)); + expect(operations.single.operationType, 'dav.create'); + expect( + _createRaw(operations.single), + contains('SUMMARY:Edited before upload'), + ); + expect(_createRaw(operations.single), contains('LOCATION:Room 4')); + + await calendarRepository.deleteLocalEvent(events.single.id); + + expect(await database.select(database.calendarEvents).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + expect(notificationChanges, 3); + }, + ); + + test( + 'confirmed event update retains raw baseline and unknown data', + () async { + const href = '${_collectionHref}confirmed-event.ics'; + final baseline = _eventResource( + uid: 'confirmed-event@example.test', + summary: 'Server title', + extra: const ['X-KEEP-EXACT;X-QUOTE="a,b":opaque'], + ); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: 'W/"event-etag"', body: baseline), + ]); + var event = await database.select(database.calendarEvents).getSingle(); + expect(event.providerRecurringEventId, isNull); + + await calendarRepository.updateLocalEvent( + _draftForEvent(event).copyWith(title: 'Local title'), + ); + + var operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + expect(operation.baselineEtag, 'W/"event-etag"'); + expect(operation.baselineRawIcs, baseline); + final patch = DavMutationPatch.fromJsonString( + operation.mutationPatchJson!, + ); + final candidate = patch.applyTo(operation.baselineRawIcs!, nowUtc: _now); + expect(candidate, contains('SUMMARY:Local title')); + expect(candidate, contains('X-KEEP-EXACT;X-QUOTE="a,b":opaque')); + + event = await database.select(database.calendarEvents).getSingle(); + expect(event.title, 'Local title'); + expect(event.syncStatus, 'pending'); + + await calendarRepository.deleteLocalEvent(event.id); + + operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.delete'); + expect(operation.baselineEtag, 'W/"event-etag"'); + expect(operation.baselineRawIcs, baseline); + expect( + (await database.select(database.calendarEvents).getSingle()).isDeleted, + isTrue, + ); + }, + ); + + test( + 'DAV event attendee mutation is rejected without local side effects', + () async { + await expectLater( + calendarRepository.createLocalEvent( + _newEventDraft().copyWith( + title: 'Invitation', + attendees: const [EventAttendeeDraft(email: 'guest@example.test')], + ), + ), + throwsA(isA()), + ); + + expect(await database.select(database.calendarEvents).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + }, + ); + + test('one generated occurrence edit adds a detached exception', () async { + const href = '${_collectionHref}recurring-event.ics'; + final baseline = _recurringEventResource(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"series-etag"', body: baseline), + ]); + final occurrence = (await database.select(database.calendarEvents).get()) + .singleWhere( + (event) => event.occurrenceKey!.contains('2026-08-10T09:00:00'), + ); + + await calendarRepository.updateLocalEvent( + _draftForEvent(occurrence).copyWith( + title: 'Only this occurrence', + start: DateTime.utc(2026, 8, 10, 13), + end: DateTime.utc(2026, 8, 10, 14), + recurringMutationScope: RecurringEventMutationScope.singleOccurrence, + ), + ); + + final operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + expect(operation.baselineRawIcs, baseline); + final patch = DavMutationPatch.fromJsonString(operation.mutationPatchJson!); + expect(patch.scope, DavMutationScope.occurrence); + final candidate = patch.applyTo(baseline, nowUtc: _now); + final semantic = IcalSemanticDocument.parse(candidate); + expect(semantic.components, hasLength(3)); + final exception = semantic.components.singleWhere( + (component) => component.recurrenceId?.rawValue == '20260810T090000Z', + ); + expect(exception.summary, 'Only this occurrence'); + expect(exception.start?.rawValue, '20260810T130000Z'); + expect(candidate, contains('X-SERIES-KEEP:opaque')); + expect(candidate, contains('SUMMARY:Existing exception')); + + final projected = (await database.select(database.calendarEvents).get()) + .singleWhere( + (event) => event.occurrenceKey!.contains('2026-08-10T09:00:00'), + ); + expect(projected.title, 'Only this occurrence'); + expect(projected.startDateTime, '2026-08-10T13:00:00.000Z'); + expect(projected.syncStatus, 'pending'); + + await calendarRepository.updateLocalEvent( + _draftForEvent(projected).copyWith( + title: 'Edited again before sync', + start: DateTime.utc(2026, 8, 10, 15), + end: DateTime.utc(2026, 8, 10, 16), + recurringMutationScope: RecurringEventMutationScope.singleOccurrence, + ), + ); + + final coalescedOperation = await database + .select(database.pendingOps) + .getSingle(); + final coalescedPatch = DavMutationPatch.fromJsonString( + coalescedOperation.mutationPatchJson!, + ); + expect(coalescedPatch.scope, DavMutationScope.occurrence); + final coalescedCandidate = coalescedPatch.applyTo(baseline, nowUtc: _now); + final coalescedException = IcalSemanticDocument.parse(coalescedCandidate) + .components + .singleWhere( + (component) => component.recurrenceId?.rawValue == '20260810T090000Z', + ); + expect(coalescedException.summary, 'Edited again before sync'); + expect(coalescedException.start?.rawValue, '20260810T150000Z'); + expect( + RegExp('RECURRENCE-ID:20260810T090000Z').allMatches(coalescedCandidate), + hasLength(1), + ); + }); + + test('existing detached exception is patched in place', () async { + const href = '${_collectionHref}recurring-exception.ics'; + final baseline = _recurringEventResource(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"series-etag"', body: baseline), + ]); + final occurrence = (await database.select(database.calendarEvents).get()) + .singleWhere((event) => event.recurrenceIdKey != null); + + await calendarRepository.updateLocalEvent( + _draftForEvent(occurrence).copyWith( + title: 'Edited exception', + recurringMutationScope: RecurringEventMutationScope.singleOccurrence, + ), + ); + + final operation = await database.select(database.pendingOps).getSingle(); + final patch = DavMutationPatch.fromJsonString(operation.mutationPatchJson!); + expect(patch.scope, DavMutationScope.recurrenceException); + final candidate = patch.applyTo(baseline, nowUtc: _now); + expect( + RegExp('RECURRENCE-ID:20260809T090000Z').allMatches(candidate), + hasLength(1), + ); + expect(candidate, contains('SUMMARY:Edited exception')); + expect(candidate, contains('X-SERIES-KEEP:opaque')); + }); + + test( + 'entire-series edit patches the master anchor and preserves exceptions', + () async { + const href = '${_collectionHref}whole-series.ics'; + final baseline = _recurringEventResource(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"series-etag"', body: baseline), + ]); + final occurrence = (await database.select(database.calendarEvents).get()) + .singleWhere( + (event) => event.occurrenceKey!.contains('2026-08-10T09:00:00'), + ); + + await calendarRepository.updateLocalEvent( + _draftForEvent(occurrence).copyWith( + title: 'Renamed series', + start: DateTime.utc(2026, 8, 10, 10), + end: DateTime.utc(2026, 8, 10, 11), + recurringMutationScope: RecurringEventMutationScope.entireSeries, + ), + ); + + final operation = await database.select(database.pendingOps).getSingle(); + final patch = DavMutationPatch.fromJsonString( + operation.mutationPatchJson!, + ); + expect(patch.scope, DavMutationScope.recurrenceMaster); + final candidate = patch.applyTo(baseline, nowUtc: _now); + final semantic = IcalSemanticDocument.parse(candidate); + final master = semantic.components.singleWhere( + (component) => component.recurrenceId == null, + ); + expect(master.start?.rawValue, '20260808T100000Z'); + expect(master.end?.rawValue, '20260808T110000Z'); + expect(master.summary, 'Renamed series'); + final exception = semantic.components.singleWhere( + (component) => component.recurrenceId != null, + ); + expect(exception.recurrenceId?.rawValue, '20260809T090000Z'); + expect(exception.start?.rawValue, '20260809T110000Z'); + expect(exception.summary, 'Existing exception'); + expect(candidate, contains('X-SERIES-KEEP:opaque')); + }, + ); + + test( + 'one-occurrence delete adds a cancelled exception, not a DELETE', + () async { + const href = '${_collectionHref}cancel-occurrence.ics'; + final baseline = _recurringEventResource(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"series-etag"', body: baseline), + ]); + final occurrence = (await database.select(database.calendarEvents).get()) + .singleWhere( + (event) => event.occurrenceKey!.contains('2026-08-10T09:00:00'), + ); + + await calendarRepository.deleteLocalEvent( + occurrence.id, + recurringScope: RecurringEventMutationScope.singleOccurrence, + ); + + final operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + final patch = DavMutationPatch.fromJsonString( + operation.mutationPatchJson!, + ); + final candidate = patch.applyTo(baseline, nowUtc: _now); + final cancelled = IcalSemanticDocument.parse(candidate).components + .singleWhere( + (component) => + component.recurrenceId?.rawValue == '20260810T090000Z', + ); + expect(cancelled.status, 'CANCELLED'); + expect(candidate, contains('SUMMARY:Existing exception')); + final projected = (await database.select(database.calendarEvents).get()) + .singleWhere( + (event) => event.occurrenceKey!.contains('2026-08-10T09:00:00'), + ); + expect(projected.isCancelled, isTrue); + }, + ); + + test( + 'event delete removes only VEVENT when an unknown sibling is present', + () async { + const href = '${_collectionHref}mixed-event.ics'; + final baseline = _eventWithUnknownSibling(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"mixed-event"', body: baseline), + ]); + final event = await database.select(database.calendarEvents).getSingle(); + + await calendarRepository.deleteLocalEvent(event.id); + + final operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + final candidate = DavMutationPatch.fromJsonString( + operation.mutationPatchJson!, + ).applyTo(baseline, nowUtc: _now); + expect(candidate, isNot(contains('BEGIN:VEVENT'))); + expect(candidate, contains('BEGIN:X-BUSYMAX-OPAQUE')); + expect(candidate, contains('X-KEEP:untouched')); + expect(IcalSemanticDocument.parse(candidate).components, isEmpty); + expect(await database.select(database.calendarEvents).get(), isEmpty); + }, + ); + + test( + 'task create can be completed, reopened, and cancelled while unsent', + () async { + await tasksRepository.createTask( + _taskListId, + const TaskCreateInput( + title: 'Local task', + fields: { + 'title': 'Local task', + 'microsoftDueDateTime': { + 'dateTime': '2026-08-09T09:30:00', + 'timeZone': 'America/Vancouver', + }, + 'microsoftDueTimeZone': 'America/Vancouver', + 'categories': ['Work'], + }, + ), + ); + + var task = await database.select(database.tasks).getSingle(); + var operation = await database.select(database.pendingOps).getSingle(); + expect(task.id, startsWith('dav-local-task-')); + expect(task.davCollectionId, _collectionId); + expect(task.davObjectId, isNull); + final expectedSortOrder = _now + .difference(DateTime.utc(2001, 1, 1)) + .inSeconds; + expect(task.sortOrder, expectedSortOrder); + expect(task.position, '$expectedSortOrder'); + expect(operation.operationType, 'dav.create'); + expect(operation.taskId, task.id); + expect(_createRaw(operation), contains('BEGIN:VTODO')); + expect(_createRaw(operation), contains('DUE;TZID=America/Vancouver')); + + await tasksRepository.patchTask( + _taskListId, + task.id, + const TaskPatchInput({'status': 'completed'}), + ); + task = await database.select(database.tasks).getSingle(); + operation = await database.select(database.pendingOps).getSingle(); + expect(task.status, 'completed'); + expect(task.providerStatus, 'COMPLETED'); + expect(task.percentComplete, 100); + expect(task.completedUtc, _now.toIso8601String()); + expect(_createRaw(operation), contains('STATUS:COMPLETED')); + expect(_createRaw(operation), contains('PERCENT-COMPLETE:100')); + expect(_createRaw(operation), contains('COMPLETED:20260808T120000Z')); + + await tasksRepository.patchTask( + _taskListId, + task.id, + const TaskPatchInput({'status': 'needsAction'}), + ); + task = await database.select(database.tasks).getSingle(); + operation = await database.select(database.pendingOps).getSingle(); + expect(task.completedUtc, isNull); + expect(task.percentComplete, 99); + expect(_createRaw(operation), contains('STATUS:NEEDS-ACTION')); + expect(_createRaw(operation), contains('PERCENT-COMPLETE:99')); + expect(_createRaw(operation), isNot(contains('COMPLETED:'))); + + await tasksRepository.deleteTask(_taskListId, task.id); + expect(await database.select(database.tasks).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + }, + ); + + test('task create rolls back when due precedes start', () async { + await expectLater( + tasksRepository.createTask( + _taskListId, + const TaskCreateInput( + title: 'Invalid range', + fields: { + 'title': 'Invalid range', + 'microsoftDueDateTime': { + 'dateTime': '2026-08-09', + 'timeZone': 'America/Vancouver', + }, + 'microsoftDueTimeZone': 'America/Vancouver', + 'microsoftStartDateTime': { + 'dateTime': '2026-08-10', + 'timeZone': 'America/Vancouver', + }, + 'microsoftStartTimeZone': 'America/Vancouver', + }, + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavTaskDueBeforeStart', + ), + ), + ); + + expect(await database.select(database.tasks).get(), isEmpty); + expect(await database.select(database.pendingOps).get(), isEmpty); + }); + + test('rejected local task create can be corrected and requeued', () async { + await tasksRepository.createTask( + _taskListId, + const TaskCreateInput( + title: 'Rejected task', + fields: { + 'title': 'Rejected task', + 'microsoftDueDateTime': { + 'dateTime': '2026-08-10', + 'timeZone': 'America/Vancouver', + }, + 'microsoftDueTimeZone': 'America/Vancouver', + 'microsoftStartDateTime': { + 'dateTime': '2026-08-09', + 'timeZone': 'America/Vancouver', + }, + 'microsoftStartTimeZone': 'America/Vancouver', + }, + ), + ); + await database + .update(database.pendingOps) + .write( + const PendingOpsCompanion( + state: Value('failed'), + retryClassification: Value('permanent'), + nextAttemptAtUtc: Value('9999-12-31T23:59:59.999Z'), + lastErrorCode: Value('DavMalformedResource'), + lastErrorMessage: Value( + 'The DAV server could not update the object.', + ), + ), + ); + final localTask = await database.select(database.tasks).getSingle(); + + await tasksRepository.patchTask( + _taskListId, + localTask.id, + const TaskPatchInput({'title': 'Corrected task'}), + ); + + final operation = await database.select(database.pendingOps).getSingle(); + expect(operation.state, 'pending'); + expect(operation.lastErrorCode, isNull); + expect(_createRaw(operation), contains('SUMMARY:Corrected task')); + expect( + (await database.select(database.tasks).getSingle()).title, + 'Corrected task', + ); + }); + + test('confirmed task updates preserve extensions and exact ETag', () async { + const href = '${_collectionHref}confirmed-task.ics'; + final baseline = _taskResource( + uid: 'confirmed-task@example.test', + summary: 'Server task', + extra: const ['X-UNKNOWN-TASK;P="q,r":retain-me'], + ); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"task-etag"', body: baseline), + ]); + var task = await database.select(database.tasks).getSingle(); + + await tasksRepository.patchTask( + _taskListId, + task.id, + const TaskPatchInput({'title': 'Local task', 'status': 'completed'}), + ); + + var operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + expect(operation.baselineEtag, '"task-etag"'); + expect(operation.baselineRawIcs, baseline); + var patch = DavMutationPatch.fromJsonString(operation.mutationPatchJson!); + var candidate = patch.applyTo(operation.baselineRawIcs!, nowUtc: _later); + expect(candidate, contains('SUMMARY:Local task')); + expect(candidate, contains('COMPLETED:20260808T120000Z')); + expect(candidate, contains('X-UNKNOWN-TASK;P="q,r":retain-me')); + + task = await database.select(database.tasks).getSingle(); + await tasksRepository.patchTask( + _taskListId, + task.id, + const TaskPatchInput({'status': 'needsAction'}), + ); + operation = await database.select(database.pendingOps).getSingle(); + patch = DavMutationPatch.fromJsonString(operation.mutationPatchJson!); + candidate = patch.applyTo(operation.baselineRawIcs!, nowUtc: _later); + expect(candidate, contains('SUMMARY:Local task')); + expect(candidate, contains('STATUS:NEEDS-ACTION')); + expect(candidate, isNot(contains('COMPLETED:'))); + expect(candidate, contains('X-UNKNOWN-TASK;P="q,r":retain-me')); + + task = await database.select(database.tasks).getSingle(); + await tasksRepository.deleteTask(_taskListId, task.id); + operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.delete'); + expect(operation.baselineEtag, '"task-etag"'); + expect(operation.baselineRawIcs, baseline); + }); + + test( + 'task delete removes only VTODO when an unknown sibling is present', + () async { + const href = '${_collectionHref}mixed-task.ics'; + final baseline = _taskWithUnknownSibling(); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"mixed-task"', body: baseline), + ]); + final task = await database.select(database.tasks).getSingle(); + + await tasksRepository.deleteTask(_taskListId, task.id); + + final operation = await database.select(database.pendingOps).getSingle(); + expect(operation.operationType, 'dav.update'); + final candidate = DavMutationPatch.fromJsonString( + operation.mutationPatchJson!, + ).applyTo(baseline, nowUtc: _now); + expect(candidate, isNot(contains('BEGIN:VTODO'))); + expect(candidate, contains('BEGIN:X-BUSYMAX-OPAQUE')); + expect(candidate, contains('X-KEEP:untouched')); + expect(IcalSemanticDocument.parse(candidate).components, isEmpty); + expect(await database.select(database.tasks).get(), isEmpty); + }, + ); + + test('task hierarchy resolves IDs and prevents cycles', () async { + const parentHref = '${_collectionHref}parent.ics'; + const childHref = '${_collectionHref}child.ics'; + await _commitObjects(objectRepository, [ + _prepared( + href: parentHref, + etag: '"parent"', + body: _taskResource(uid: 'parent@example.test', summary: 'Parent'), + ), + _prepared( + href: childHref, + etag: '"child"', + body: _taskResource( + uid: 'child@example.test', + summary: 'Child', + extra: const ['RELATED-TO;RELTYPE=PARENT:parent@example.test'], + ), + ), + ]); + final rows = await database.select(database.tasks).get(); + final parent = rows.singleWhere( + (task) => task.icalUid == 'parent@example.test', + ); + var child = rows.singleWhere( + (task) => task.icalUid == 'child@example.test', + ); + expect(child.parentUid, parent.icalUid); + expect(child.parent, parent.id); + + await expectLater( + tasksRepository.moveTask( + TaskMoveInput( + sourceTaskListId: _taskListId, + taskId: parent.id, + parentTaskId: child.id, + ), + ), + throwsA(isA()), + ); + + await tasksRepository.moveTask( + TaskMoveInput(sourceTaskListId: _taskListId, taskId: child.id), + ); + child = + await (database.select(database.tasks)..where( + (row) => + row.accountId.equals(_accountId) & row.id.equals(child.id), + )) + .getSingle(); + expect(child.parent, isNull); + expect(child.parentUid, isNull); + final operations = await database.select(database.pendingOps).get(); + expect(operations, hasLength(2)); + final childOperation = operations.singleWhere( + (operation) => operation.davMemberHref == childHref, + ); + final childPatch = DavMutationPatch.fromJsonString( + childOperation.mutationPatchJson!, + ); + final childCandidate = childPatch.applyTo( + childOperation.baselineRawIcs!, + nowUtc: _now, + ); + expect(childCandidate, isNot(contains('RELATED-TO;RELTYPE=PARENT'))); + expect(childCandidate, contains('X-APPLE-SORT-ORDER:0')); + final parentOperation = operations.singleWhere( + (operation) => operation.davMemberHref == parentHref, + ); + final parentCandidate = DavMutationPatch.fromJsonString( + parentOperation.mutationPatchJson!, + ).applyTo(parentOperation.baselineRawIcs!, nowUtc: _now); + expect(parentCandidate, contains('X-APPLE-SORT-ORDER:1')); + }); + + test('deleting a parent queues every child before the parent', () async { + const parentHref = '${_collectionHref}delete-parent.ics'; + const childHref = '${_collectionHref}delete-child.ics'; + await _commitObjects(objectRepository, [ + _prepared( + href: parentHref, + etag: '"parent"', + body: _taskResource( + uid: 'delete-parent@example.test', + summary: 'Parent', + ), + ), + _prepared( + href: childHref, + etag: '"child"', + body: _taskResource( + uid: 'delete-child@example.test', + summary: 'Child', + extra: const ['RELATED-TO;RELTYPE=PARENT:delete-parent@example.test'], + ), + ), + ]); + final parent = (await database.select(database.tasks).get()).singleWhere( + (task) => task.icalUid == 'delete-parent@example.test', + ); + + await tasksRepository.deleteTask(_taskListId, parent.id); + + final operations = await database.select(database.pendingOps).get(); + expect(operations, hasLength(2)); + final childDelete = operations.singleWhere( + (operation) => operation.davMemberHref == childHref, + ); + final parentDelete = operations.singleWhere( + (operation) => operation.davMemberHref == parentHref, + ); + expect(childDelete.dependsOnOpId, isNull); + expect(parentDelete.dependsOnOpId, childDelete.id); + expect( + (await database.select(database.tasks).get()).every( + (task) => task.pendingDelete, + ), + isTrue, + ); + }); + + test( + 'duplicate recursively preserves task data and reparents children', + () async { + const parentHref = '${_collectionHref}duplicate-parent.ics'; + const childHref = '${_collectionHref}duplicate-child.ics'; + await _commitObjects(objectRepository, [ + _prepared( + href: parentHref, + etag: '"duplicate-parent"', + body: _taskResource( + uid: 'duplicate-parent@example.test', + summary: 'Duplicate parent', + extra: const [ + 'CREATED:20260801T120000Z', + 'PRIORITY:3', + 'LOCATION:Room 4', + 'URL:https://cloud.example.test/tasks/parent', + 'CATEGORIES:Work,Planning', + 'X-KEEP:opaque', + ], + ), + ), + _prepared( + href: childHref, + etag: '"duplicate-child"', + body: _taskResource( + uid: 'duplicate-child@example.test', + summary: 'Duplicate child', + extra: const [ + 'CREATED:20260801T120100Z', + 'RELATED-TO:duplicate-parent@example.test', + ], + ), + ), + ]); + final sourceParent = (await database.select(database.tasks).get()) + .singleWhere( + (task) => task.icalUid == 'duplicate-parent@example.test', + ); + + final duplicateParentId = await tasksRepository.duplicateTask( + _taskListId, + sourceParent.id, + ); + + final tasks = await database.select(database.tasks).get(); + expect(tasks, hasLength(4)); + final duplicateParent = tasks.singleWhere( + (task) => task.id == duplicateParentId, + ); + final duplicateChild = tasks.singleWhere( + (task) => task.localCreated && task.parent == duplicateParentId, + ); + expect(duplicateChild.parentUid, duplicateParent.icalUid); + final expectedSortOrder = _now + .difference(DateTime.utc(2001, 1, 1)) + .inSeconds; + expect(duplicateParent.sortOrder, expectedSortOrder); + expect(duplicateChild.sortOrder, expectedSortOrder); + + final operations = await database.select(database.pendingOps).get(); + expect(operations, hasLength(2)); + final parentCreate = operations.singleWhere( + (operation) => operation.taskId == duplicateParent.id, + ); + final childCreate = operations.singleWhere( + (operation) => operation.taskId == duplicateChild.id, + ); + expect(parentCreate.operationType, 'dav.create'); + expect(childCreate.operationType, 'dav.create'); + expect(childCreate.dependsOnOpId, parentCreate.id); + expect(_createRaw(parentCreate), contains('PRIORITY:3')); + expect(_createRaw(parentCreate), contains('LOCATION:Room 4')); + expect(_createRaw(parentCreate), contains('X-KEEP:opaque')); + expect( + IcalSemanticDocument.parse( + _createRaw(childCreate), + ).components.single.parentUid, + duplicateParent.icalUid, + ); + }, + ); + + test('native task export includes the current pending overlay', () async { + const href = '${_collectionHref}export.ics'; + final baseline = _taskResource( + uid: 'export@example.test', + summary: 'Original title', + extra: const ['X-KEEP:opaque'], + ); + await _commitObjects(objectRepository, [ + _prepared(href: href, etag: '"export"', body: baseline), + ]); + final task = await database.select(database.tasks).getSingle(); + + expect( + await tasksRepository.nativeTaskExport(_taskListId, task.id), + baseline, + ); + + await tasksRepository.patchTask( + _taskListId, + task.id, + const TaskPatchInput({'title': 'Pending title'}), + ); + final exported = await tasksRepository.nativeTaskExport( + _taskListId, + task.id, + ); + expect(exported, contains('SUMMARY:Pending title')); + expect(exported, contains('X-KEEP:opaque')); + }); + + test('cross-list move queues a child-first DAV subtree move', () async { + await _seedDestinationTaskList(database); + const parentHref = '${_collectionHref}move-parent.ics'; + const childHref = '${_collectionHref}move-child.ics'; + await _commitObjects(objectRepository, [ + _prepared( + href: parentHref, + etag: '"move-parent"', + body: _taskResource( + uid: 'move-parent@example.test', + summary: 'Move parent', + ), + ), + _prepared( + href: childHref, + etag: '"move-child"', + body: _taskResource( + uid: 'move-child@example.test', + summary: 'Move child', + extra: const ['RELATED-TO;RELTYPE=PARENT:move-parent@example.test'], + ), + ), + ]); + final sourceTasks = await database.select(database.tasks).get(); + final parent = sourceTasks.singleWhere( + (task) => task.icalUid == 'move-parent@example.test', + ); + final child = sourceTasks.singleWhere( + (task) => task.icalUid == 'move-child@example.test', + ); + + await tasksRepository.moveTask( + TaskMoveInput( + sourceTaskListId: _taskListId, + taskId: parent.id, + destinationTaskListId: _destinationTaskListId, + ), + ); + + final operations = await database.select(database.pendingOps).get(); + expect(operations, hasLength(2)); + final childMove = operations.singleWhere( + (operation) => operation.taskId == child.id, + ); + final parentMove = operations.singleWhere( + (operation) => operation.taskId == parent.id, + ); + expect(childMove.operationType, 'dav.move'); + expect(parentMove.operationType, 'dav.move'); + expect(childMove.destinationCollectionId, _destinationCollectionId); + expect(parentMove.destinationCollectionId, _destinationCollectionId); + expect(childMove.dependsOnOpId, isNull); + expect(parentMove.dependsOnOpId, childMove.id); + + final moved = await database.select(database.tasks).get(); + expect( + moved.every((task) => task.taskListId == _destinationTaskListId), + isTrue, + ); + expect( + moved.every((task) => task.davCollectionId == _destinationCollectionId), + isTrue, + ); + expect(moved.singleWhere((task) => task.id == child.id).parent, parent.id); + }); + + test('clear completed queues Nextcloud closed root task trees', () async { + const completedHref = '${_collectionHref}completed.ics'; + const statusOnlyHref = '${_collectionHref}status-only.ics'; + const completedDateOnlyHref = '${_collectionHref}completed-date-only.ics'; + const cancelledHref = '${_collectionHref}cancelled.ics'; + const percentOnlyHref = '${_collectionHref}percent-only.ics'; + const openParentHref = '${_collectionHref}open-parent.ics'; + const closedChildHref = '${_collectionHref}closed-child.ics'; + await _commitObjects(objectRepository, [ + _prepared( + href: completedHref, + etag: '"completed"', + body: _taskResource( + uid: 'completed@example.test', + summary: 'Completed', + extra: const [ + 'STATUS:COMPLETED', + 'PERCENT-COMPLETE:100', + 'COMPLETED:20260808T110000Z', + ], + ), + ), + _prepared( + href: statusOnlyHref, + etag: '"status-only"', + body: _taskResource( + uid: 'status-only@example.test', + summary: 'Status only', + extra: const ['STATUS:COMPLETED', 'PERCENT-COMPLETE:50'], + ), + ), + _prepared( + href: completedDateOnlyHref, + etag: '"completed-date-only"', + body: _taskResource( + uid: 'completed-date-only@example.test', + summary: 'Completed date only', + extra: const ['COMPLETED:20260808T120000Z'], + ), + ), + _prepared( + href: cancelledHref, + etag: '"cancelled"', + body: _taskResource( + uid: 'cancelled@example.test', + summary: 'Cancelled', + extra: const ['STATUS:CANCELLED'], + ), + ), + _prepared( + href: percentOnlyHref, + etag: '"percent-only"', + body: _taskResource( + uid: 'percent-only@example.test', + summary: 'Percent only', + extra: const ['PERCENT-COMPLETE:100'], + ), + ), + _prepared( + href: openParentHref, + etag: '"open-parent"', + body: _taskResource( + uid: 'open-parent@example.test', + summary: 'Open parent', + ), + ), + _prepared( + href: closedChildHref, + etag: '"closed-child"', + body: _taskResource( + uid: 'closed-child@example.test', + summary: 'Closed child', + extra: const [ + 'RELATED-TO:open-parent@example.test', + 'STATUS:COMPLETED', + ], + ), + ), + ]); + + await tasksRepository.clearCompleted(_taskListId); + + final operations = await database.select(database.pendingOps).get(); + expect(operations, hasLength(4)); + expect( + operations.every((operation) => operation.operationType == 'dav.delete'), + isTrue, + ); + expect(operations.map((operation) => operation.davMemberHref).toSet(), { + completedHref, + statusOnlyHref, + completedDateOnlyHref, + cancelledHref, + }); + final tasks = await database.select(database.tasks).get(); + bool pendingDelete(String uid) => + tasks.singleWhere((task) => task.icalUid == uid).pendingDelete; + expect(pendingDelete('completed@example.test'), isTrue); + expect(pendingDelete('status-only@example.test'), isTrue); + expect(pendingDelete('completed-date-only@example.test'), isTrue); + expect(pendingDelete('cancelled@example.test'), isTrue); + expect(pendingDelete('percent-only@example.test'), isFalse); + expect(pendingDelete('open-parent@example.test'), isFalse); + expect(pendingDelete('closed-child@example.test'), isFalse); + }); +} + +EventEditorDraft _newEventDraft() => EventEditorDraft.newEvent( + accountId: _accountId, + sourceId: _sourceId, + providerCalendarId: _collectionHref, + start: DateTime.utc(2026, 8, 8, 9), + end: DateTime.utc(2026, 8, 8, 10), +); + +EventEditorDraft _draftForEvent(CalendarEvent event) => + EventEditorDraft.existing( + eventId: event.id, + accountId: event.accountId, + sourceId: event.calendarSourceId, + providerCalendarId: event.providerCalendarId, + providerRecurringEventId: event.providerRecurringEventId, + title: event.title, + allDay: event.allDay, + start: event.allDay + ? DateTime.parse(event.startDate!) + : DateTime.parse(event.startDateTime!), + end: event.allDay + ? DateTime.parse(event.endDate!) + : DateTime.parse(event.endDateTime!), + startTimeZone: event.startTimeZone, + endTimeZone: event.endTimeZone, + description: event.description, + location: event.location, + recurrence: event.recurrenceJson == null + ? null + : jsonDecode(event.recurrenceJson!), + reminders: event.remindersJson == null + ? null + : jsonDecode(event.remindersJson!), + categories: event.categoriesJson == null + ? const [] + : (jsonDecode(event.categoriesJson!) as List).cast(), + showAs: event.transparencyOrShowAs, + visibilityOrSensitivity: event.visibility, + ); + +String _createRaw(PendingOp operation) => + (jsonDecode(operation.requestJson) as Map)['rawIcs']! as String; + +DavPreparedObject _prepared({ + required String href, + required String etag, + required String body, +}) => DavPreparedObject.parse( + hrefKey: href, + requestUri: Uri.parse('https://cloud.example.test$href'), + etag: etag, + contentType: 'text/calendar; charset=utf-8', + rawIcsBody: body, +); + +Future _commitObjects( + DavObjectRepository repository, + List objects, +) { + return repository.commit( + DavCollectionCommit( + accountId: _accountId, + collectionId: _collectionId, + provider: BusyProvider.nextcloud, + objects: objects, + deletedHrefKeys: const {}, + completeMembership: true, + membershipHrefKeys: {for (final object in objects) object.hrefKey}, + finalCursorKind: 'dav_sync_token', + finalCursorValue: 'sync-token-1', + baselineGeneration: 1, + completedAtUtc: _now, + projectionRangeStartUtc: DateTime.utc(2025), + projectionRangeEndUtc: DateTime.utc(2028), + ), + ); +} + +Future _seedDavAccount(AppDatabase database) async { + final now = _now.toIso8601String(); + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: _accountId, + provider: 'nextcloud', + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: _collectionId, + accountId: _accountId, + hrefKey: _collectionHref, + requestUri: 'https://cloud.example.test$_collectionHref', + displayName: 'Work', + supportedComponentMask: const Value(3), + supportedReportsJson: Value( + jsonEncode([ + '{DAV:}sync-collection', + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + '{urn:ietf:params:xml:ns:caldav}calendar-query', + ]), + ), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: _sourceId, + accountId: _accountId, + provider: 'nextcloud', + providerCalendarId: _collectionHref, + davCollectionId: const Value(_collectionId), + summary: 'Work', + createdAtLocal: _now.millisecondsSinceEpoch, + updatedAtLocal: _now.millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: _accountId, + id: _taskListId, + davCollectionId: const Value(_collectionId), + title: 'Work', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} + +Future _seedDestinationTaskList(AppDatabase database) async { + final now = _now.toIso8601String(); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: _destinationCollectionId, + accountId: _accountId, + hrefKey: _destinationCollectionHref, + requestUri: 'https://cloud.example.test$_destinationCollectionHref', + displayName: 'Home', + supportedComponentMask: const Value(2), + supportedReportsJson: Value( + jsonEncode([ + '{DAV:}sync-collection', + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ]), + ), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(false), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: _accountId, + id: _destinationTaskListId, + davCollectionId: const Value(_destinationCollectionId), + title: 'Home', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} + +String _eventResource({ + required String uid, + required String summary, + List extra = const [], +}) => _ical([ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//BusyMax integration test//EN', + 'BEGIN:VEVENT', + 'UID:$uid', + 'DTSTAMP:20260808T110000Z', + 'DTSTART:20260808T090000Z', + 'DTEND:20260808T100000Z', + 'SUMMARY:$summary', + ...extra, + 'END:VEVENT', + 'END:VCALENDAR', +]); + +String _recurringEventResource() => _ical(const [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//BusyMax integration test//EN', + 'BEGIN:VEVENT', + 'UID:series@example.test', + 'DTSTAMP:20260808T110000Z', + 'DTSTART:20260808T090000Z', + 'DTEND:20260808T100000Z', + 'RRULE:FREQ=DAILY;COUNT=3', + 'SUMMARY:Server series', + 'X-SERIES-KEEP:opaque', + 'END:VEVENT', + 'BEGIN:VEVENT', + 'UID:series@example.test', + 'RECURRENCE-ID:20260809T090000Z', + 'DTSTAMP:20260808T110000Z', + 'DTSTART:20260809T110000Z', + 'DTEND:20260809T120000Z', + 'SUMMARY:Existing exception', + 'X-EXCEPTION-KEEP:opaque', + 'END:VEVENT', + 'END:VCALENDAR', +]); + +String _eventWithUnknownSibling() => _ical(const [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//BusyMax integration test//EN', + 'BEGIN:VEVENT', + 'UID:mixed-event@example.test', + 'DTSTAMP:20260808T110000Z', + 'DTSTART:20260808T090000Z', + 'DTEND:20260808T100000Z', + 'SUMMARY:Projected event', + 'END:VEVENT', + 'BEGIN:X-BUSYMAX-OPAQUE', + 'X-KEEP:untouched', + 'END:X-BUSYMAX-OPAQUE', + 'END:VCALENDAR', +]); + +String _taskResource({ + required String uid, + required String summary, + List extra = const [], +}) => _ical([ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//BusyMax integration test//EN', + 'BEGIN:VTODO', + 'UID:$uid', + 'DTSTAMP:20260808T110000Z', + 'SUMMARY:$summary', + ...extra, + 'END:VTODO', + 'END:VCALENDAR', +]); + +String _taskWithUnknownSibling() => _ical(const [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//BusyMax integration test//EN', + 'BEGIN:VTODO', + 'UID:mixed-task@example.test', + 'DTSTAMP:20260808T110000Z', + 'SUMMARY:Projected task', + 'END:VTODO', + 'BEGIN:X-BUSYMAX-OPAQUE', + 'X-KEEP:untouched', + 'END:X-BUSYMAX-OPAQUE', + 'END:VCALENDAR', +]); + +String _ical(List lines) => '${lines.join('\r\n')}\r\n'; + +const _accountId = 'nextcloud:alex'; +const _collectionId = 'collection'; +const _collectionHref = '/remote.php/dav/calendars/alex/work/'; +const _sourceId = 'dav-calendar-collection'; +const _taskListId = 'dav-task-list-collection'; +const _destinationCollectionId = 'destination-collection'; +const _destinationCollectionHref = '/remote.php/dav/calendars/alex/home/'; +const _destinationTaskListId = 'dav-task-list-destination-collection'; +final _now = DateTime.utc(2026, 8, 8, 12); +final _later = DateTime.utc(2026, 8, 9, 12); diff --git a/test/dav/mutation/dav_task_list_mutation_service_test.dart b/test/dav/mutation/dav_task_list_mutation_service_test.dart new file mode 100644 index 0000000..b48497c --- /dev/null +++ b/test/dav/mutation/dav_task_list_mutation_service_test.dart @@ -0,0 +1,336 @@ +import 'dart:io'; + +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/dav/mutation/dav_task_list_mutation_service.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:drift/drift.dart' hide isNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + late AppDatabase database; + late InMemorySecretStore secrets; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + secrets = InMemorySecretStore(); + await _seedAccount(database, secrets); + }); + + tearDown(() => database.close()); + + test('collection member naming matches cdav-library token rules', () { + final home = Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/', + ); + + expect( + nextcloudCollectionMemberName( + ' Résumé List ', + homeUri: home, + existingCollectionUris: const [], + ), + 'rsum-list', + ); + expect( + nextcloudCollectionMemberName( + 'Work', + homeUri: home, + existingCollectionUris: [ + home.resolve('work/'), + home.resolve('work-1/'), + ], + ), + 'work-2', + ); + expect( + nextcloudCollectionMemberName( + '---', + homeUri: home, + existingCollectionUris: const [], + ), + '-', + ); + }); + + test( + 'create sends the exact VTODO extended MKCOL shape and refreshes', + () async { + late http.Request request; + var refreshes = 0; + final service = _service( + database, + secrets, + MockClient((incoming) async { + request = incoming; + return http.Response('', HttpStatus.created); + }), + refresh: () async => refreshes += 1, + ); + + await service.createTaskList('Project Tasks'); + + expect(request.method, 'MKCOL'); + expect(request.url.path, endsWith('/project-tasks')); + expect(request.headers['content-type'], contains('application/xml')); + expect(request.body, contains(''), + ); + expect(request.body, contains('Project Tasks')); + expect( + request.body, + contains('$nextcloudDefaultTaskListColor'), + ); + expect(request.body, contains('1')); + expect(request.body, contains('')); + expect(refreshes, 1); + }, + ); + + test('a lost MKCOL response is reconciled by a matching PROPFIND', () async { + var requests = 0; + var refreshes = 0; + final service = _service( + database, + secrets, + MockClient((request) async { + requests += 1; + if (request.method == 'MKCOL') { + throw const SocketException('connection closed after commit'); + } + expect(request.method, 'PROPFIND'); + expect(request.headers['depth'], '0'); + return http.Response(_createdCollectionMultistatus('Recovered'), 207); + }), + refresh: () async => refreshes += 1, + ); + + await service.createTaskList('Recovered'); + + expect(requests, 2); + expect(refreshes, 1); + }); + + test('rename requires write-properties and validates multistatus', () async { + await _seedCollection(database, privileges: const ['{DAV:}write']); + late http.Request request; + var refreshes = 0; + final service = _service( + database, + secrets, + MockClient((incoming) async { + request = incoming; + return http.Response(_successfulProppatch, 207); + }), + refresh: () async => refreshes += 1, + ); + + await service.renameTaskList('collection', 'Home & Family'); + + expect(request.method, 'PROPPATCH'); + expect(request.url.path, endsWith('/tasks/')); + expect(request.body, contains(' http.Response(_forbiddenProppatch, 207)), + refresh: () async => refreshes += 1, + ); + + await expectLater( + service.renameTaskList('collection', 'Rejected'), + throwsA( + isA() + .having((error) => error.kind, 'kind', DavErrorKind.authorization) + .having((error) => error.statusCode, 'statusCode', 403), + ), + ); + expect(refreshes, 0); + }); + + test('read-only shared collection can be unshared with DELETE', () async { + await _seedCollection( + database, + ownerHref: '/remote.php/dav/principals/users/bob/', + privileges: const ['{DAV:}read'], + readOnly: true, + ); + late http.Request request; + var refreshes = 0; + final service = _service( + database, + secrets, + MockClient((incoming) async { + request = incoming; + return http.Response('', HttpStatus.noContent); + }), + refresh: () async => refreshes += 1, + ); + + await service.deleteTaskList('collection'); + + expect(request.method, 'DELETE'); + expect(refreshes, 1); + }); + + test('read-only owned collection is rejected before DELETE', () async { + await _seedCollection( + database, + privileges: const ['{DAV:}read'], + readOnly: true, + ); + var requests = 0; + final service = _service( + database, + secrets, + MockClient((_) async { + requests += 1; + return http.Response('', HttpStatus.noContent); + }), + ); + + await expectLater( + service.deleteTaskList('collection'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavCollectionReadOnly', + ), + ), + ); + expect(requests, 0); + }); +} + +DavTaskListMutationService _service( + AppDatabase database, + SecretStore secrets, + http.Client client, { + Future Function()? refresh, +}) { + var correlation = 0; + return DavTaskListMutationService( + database: database, + secretStore: secrets, + httpClient: client, + accountId: 'account', + refreshAfterMutation: refresh ?? () async {}, + correlationIdFactory: () => 'correlation-${correlation += 1}', + ); +} + +Future _seedAccount( + AppDatabase database, + InMemorySecretStore secrets, +) async { + const now = '2026-08-09T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.davAccountServices) + .insert( + DavAccountServicesCompanion.insert( + accountId: 'account', + canonicalServiceUri: 'https://cloud.example.test/remote.php/dav/', + canonicalOrigin: 'https://cloud.example.test', + principalHref: const Value('/remote.php/dav/principals/users/alex/'), + calendarHomeHref: const Value( + 'https://cloud.example.test/remote.php/dav/calendars/alex/', + ), + discoveredAtUtc: now, + ), + ); + await secrets.saveCredential( + 'account', + NextcloudSecretRecord( + canonicalServer: Uri.parse('https://cloud.example.test'), + loginName: 'alex', + appPassword: 'application-password', + ), + ); +} + +Future _seedCollection( + AppDatabase database, { + String ownerHref = '/remote.php/dav/principals/users/alex/', + required List privileges, + bool readOnly = false, +}) async { + const now = '2026-08-09T12:00:00.000Z'; + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: '/remote.php/dav/calendars/alex/tasks/', + requestUri: + 'https://cloud.example.test/remote.php/dav/calendars/alex/tasks/', + displayName: 'Tasks', + supportedComponentMask: const Value(davComponentTodo), + currentUserPrivilegesJson: Value(_jsonStrings(privileges)), + ownerHref: Value(ownerHref), + readOnly: Value(readOnly), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); +} + +String _jsonStrings(List values) => + '[${values.map((value) => '"$value"').join(',')}]'; + +String _createdCollectionMultistatus(String displayName) => + '' + '' + '/remote.php/dav/calendars/alex/recovered/' + '' + '' + '$displayName' + '' + '' + 'HTTP/1.1 200 OK' + ''; + +const _successfulProppatch = + '' + '' + '/remote.php/dav/calendars/alex/tasks/' + '' + 'HTTP/1.1 200 OK' + ''; + +const _forbiddenProppatch = + '' + '' + '/remote.php/dav/calendars/alex/tasks/' + '' + 'HTTP/1.1 403 Forbidden' + ''; diff --git a/test/dav/nextcloud_live_integration_test.dart b/test/dav/nextcloud_live_integration_test.dart new file mode 100644 index 0000000..4b2b56f --- /dev/null +++ b/test/dav/nextcloud_live_integration_test.dart @@ -0,0 +1,1436 @@ +import 'dart:io'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_repository.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_service.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/ical/ical_semantics.dart'; +import 'package:busymax/src/dav/mutation/dav_conditional_mutation_service.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/mutation/dav_pending_operations.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/dav/sync/dav_collection_remote_client.dart'; +import 'package:busymax/src/dav/sync/dav_sync_engine.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +const _enabledVariable = 'BUSYMAX_NEXTCLOUD_LIVE'; + +void main() { + final enabled = Platform.environment[_enabledVariable] == '1'; + final largeEnabled = + enabled && Platform.environment['BUSYMAX_NEXTCLOUD_LIVE_LARGE'] == '1'; + final restartStage = + Platform.environment['BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE']; + + test( + 'exact Nextcloud server preserves DAV calendar and task semantics', + () async { + final fixture = _LiveNextcloudFixture.fromEnvironment(); + addTearDown(fixture.close); + + final initialDiscovery = await fixture.discover('live-discovery'); + expect(initialDiscovery.service.calendarHomeHref.path, isNotEmpty); + + final suffix = DateTime.now().microsecondsSinceEpoch.toString(); + final eventSlug = 'busymax-qa-events-$suffix'; + final taskSlug = 'busymax-qa-tasks-$suffix'; + final eventUri = _collectionChild( + initialDiscovery.service.calendarHomeHref, + eventSlug, + ); + final taskUri = _collectionChild( + initialDiscovery.service.calendarHomeHref, + taskSlug, + ); + await fixture.createCollection( + eventUri, + displayName: 'BusyMax QA Events', + component: 'VEVENT', + color: '#3A7BFFFF', + ); + fixture.registerCollectionForCleanup(eventUri); + await fixture.createCollection( + taskUri, + displayName: 'BusyMax QA Tasks', + component: 'VTODO', + color: '#7457D5FF', + ); + fixture.registerCollectionForCleanup(taskUri); + + final discovery = await fixture.discover('live-rediscovery'); + final eventCollection = _collectionBySlug(discovery, eventSlug); + final taskCollection = _collectionBySlug(discovery, taskSlug); + expect(eventCollection.eventProjectionEnabled, isTrue); + expect(eventCollection.capabilities.canCreateEvent, isTrue); + expect(eventCollection.color?.toUpperCase(), '#3A7BFFFF'); + expect(taskCollection.taskProjectionEnabled, isTrue); + expect(taskCollection.capabilities.canCreateTask, isTrue); + expect(taskCollection.color?.toUpperCase(), '#7457D5FF'); + + await fixture.verifyTransientReadRecovery(discovery); + await fixture.verifyEvents(eventCollection, suffix); + await fixture.verifyTasks(taskCollection, suffix); + await fixture.verifyProductionSyncEngine(eventCollection, discovery); + }, + skip: enabled + ? false + : 'Set $_enabledVariable=1 and the BUSYMAX_NEXTCLOUD_LIVE_* ' + 'credential variables to run the isolated live-server tests.', + timeout: const Timeout(Duration(minutes: 5)), + ); + + test( + 'large Nextcloud collection synchronizes and deletes without truncation', + () async { + final fixture = _LiveNextcloudFixture.fromEnvironment(); + addTearDown(fixture.close); + final discovery = await fixture.discover('live-large-discovery'); + final suffix = DateTime.now().microsecondsSinceEpoch.toString(); + final slug = 'busymax-qa-large-$suffix'; + final collectionUri = _collectionChild( + discovery.service.calendarHomeHref, + slug, + ); + await fixture.createCollection( + collectionUri, + displayName: 'BusyMax QA Large', + component: 'VEVENT', + color: '#2563EBFF', + ); + fixture.registerCollectionForCleanup(collectionUri); + final collection = _collectionBySlug( + await fixture.discover('live-large-rediscovery'), + slug, + ); + + await fixture.verifyLargeCollection(collection, suffix, memberCount: 128); + }, + skip: largeEnabled + ? false + : 'Set BUSYMAX_NEXTCLOUD_LIVE_LARGE=1 with the live-server ' + 'variables to run the 128-member collection test.', + timeout: const Timeout(Duration(minutes: 5)), + ); + + test( + 'prepare a durable Nextcloud object for server restart', + () async { + final fixture = _LiveNextcloudFixture.fromEnvironment(); + addTearDown(fixture.close); + final slug = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LIVE_RESTART_ID'); + final discovery = await fixture.discover( + 'live-restart-prepare-discovery', + ); + final collectionUri = _collectionChild( + discovery.service.calendarHomeHref, + slug, + ); + await fixture.createCollection( + collectionUri, + displayName: 'BusyMax QA Restart', + component: 'VEVENT', + color: '#0891B2FF', + ); + final collection = _collectionBySlug( + await fixture.discover('live-restart-prepare-rediscovery'), + slug, + ); + await fixture.prepareRestartObject(collection, slug); + }, + skip: enabled && restartStage == 'prepare' + ? false + : 'Set the live restart stage to prepare to create the durable probe.', + timeout: const Timeout(Duration(minutes: 2)), + ); + + test( + 'rediscover and mutate the durable object after server restart', + () async { + final fixture = _LiveNextcloudFixture.fromEnvironment(); + addTearDown(fixture.close); + final slug = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LIVE_RESTART_ID'); + final collection = _collectionBySlug( + await fixture.discover('live-restart-verify-discovery'), + slug, + ); + await fixture.verifyRestartObject(collection, slug); + await fixture.deleteCollection(collection.requestUri); + }, + skip: enabled && restartStage == 'verify' + ? false + : 'Set the live restart stage to verify after restarting the server.', + timeout: const Timeout(Duration(minutes: 2)), + ); +} + +final class _LiveNextcloudFixture { + _LiveNextcloudFixture({ + required this.authority, + required this.profile, + required this.credential, + required this.client, + required this.transport, + }); + + factory _LiveNextcloudFixture.fromEnvironment() { + final url = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LIVE_URL'); + final username = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LIVE_USERNAME'); + final password = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LIVE_PASSWORD'); + final authority = Uri.parse(url); + if (!authority.hasScheme || authority.host.isEmpty) { + throw StateError('The live Nextcloud URL is not an absolute URI.'); + } + final insecureLoopback = + authority.scheme == 'http' && + const {'127.0.0.1', '::1', 'localhost'}.contains(authority.host); + final profile = DavProviderProfile( + provider: BusyProvider.nextcloud, + bootstrapUri: authority, + calendarEnabled: true, + tasksEnabled: true, + allowCollectionMutations: false, + allowSchedulingMutations: false, + allowMove: false, + allowInsecureLoopbackForTesting: insecureLoopback, + ); + final client = http.Client(); + return _LiveNextcloudFixture( + authority: authority, + profile: profile, + credential: DavBasicCredential(username: username, password: password), + client: client, + transport: DavHttpTransport( + client: client, + profile: profile, + accountAuthority: authority, + ), + ); + } + + final Uri authority; + final DavProviderProfile profile; + final DavBasicCredential credential; + final http.Client client; + final DavHttpTransport transport; + final List _collectionsToDelete = []; + + Future discover(String correlationId) => + DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: authority, + accountId: 'nextcloud-live', + credential: credential, + ).discover(correlationId: correlationId); + + void registerCollectionForCleanup(Uri uri) { + _collectionsToDelete.add(uri); + } + + Future createCollection( + Uri uri, { + required String displayName, + required String component, + required String color, + }) async { + final response = await transport.send( + DavRequest.xml( + method: 'MKCALENDAR', + uri: uri, + accountId: 'nextcloud-live', + correlationId: 'live-create-collection', + retryClass: DavRetryClass.never, + body: + ''' + + + + $displayName + + $color + +''', + ), + credential: credential, + ); + expect(response.statusCode, anyOf(201, 204)); + } + + Future verifyEvents( + DavCollectionDiscovery collection, + String suffix, + ) async { + final remote = _collectionClient(collection, 'events'); + final mutations = _mutationService('events'); + final emptyPage = await remote.syncCollectionPage( + syncToken: '', + correlationId: 'live-events-empty-sync', + ); + expect(emptyPage.changedMembers, isEmpty); + var syncToken = emptyPage.nextSyncToken; + + final uid = 'busymax-live-event-$suffix@example.invalid'; + final event = DavNewObject( + uid: uid, + initialMemberName: 'busymax-event-$suffix.ics', + componentType: 'VEVENT', + rawIcs: _recurringEvent(uid), + ); + var result = await mutations.create( + collectionUri: collection.requestUri, + object: event, + capabilities: collection.capabilities, + correlationId: 'live-event-create', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + var current = result.canonicalObject!; + _expectEventPreservation(current.rawIcsBody!); + + final variants = []; + for (final variant in [ + ( + 'all-day', + IcalTemporalKind.date, + 'DTSTART;VALUE=DATE:20260812', + 'DTEND;VALUE=DATE:20260813', + ), + ( + 'floating', + IcalTemporalKind.floatingDateTime, + 'DTSTART:20260813T090000', + 'DTEND:20260813T100000', + ), + ( + 'utc', + IcalTemporalKind.utcDateTime, + 'DTSTART:20260814T160000Z', + 'DTEND:20260814T170000Z', + ), + ]) { + final variantUid = 'busymax-live-${variant.$1}-$suffix@example.invalid'; + final variantResult = await mutations.create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: variantUid, + initialMemberName: 'busymax-${variant.$1}-$suffix.ics', + rawIcs: _simpleEvent( + uid: variantUid, + summary: 'BusyMax ${variant.$1} event', + startLine: variant.$3, + endLine: variant.$4, + ), + componentType: 'VEVENT', + ), + capabilities: collection.capabilities, + correlationId: 'live-${variant.$1}-event-create', + ); + expect(variantResult.outcome, DavMutationOutcome.succeeded); + final canonical = variantResult.canonicalObject!; + expect( + IcalSemanticDocument.parse( + canonical.rawIcsBody!, + ).components.single.start!.kind, + variant.$2, + ); + variants.add(canonical); + } + + var page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-event-create-sync', + ); + expect(page.changedMembers, hasLength(4)); + expect( + page.changedMembers.map((member) => member.hrefKey), + contains(current.hrefKey), + ); + syncToken = page.nextSyncToken; + final fetched = await remote.fetchMembers( + page.changedMembers, + correlationId: 'live-event-multiget', + useCalendarMultiget: true, + ); + expect(fetched, hasLength(4)); + expect( + fetched.any( + (member) => member.rawIcsBody!.contains('SUMMARY:BusyMax live event'), + ), + isTrue, + ); + + final original = current; + final remoteLocationPatch = DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('LOCATION', 'Nextcloud-side location'), + ], + ); + final directUpdate = await _mutationClient('events').conditionalPut( + uri: current.requestUri, + rawIcs: remoteLocationPatch.applyTo( + current.rawIcsBody!, + nowUtc: DateTime.utc(2026, 8, 8, 12, 34, 56), + ), + correlationId: 'live-event-out-of-band-update', + ifMatch: current.etag, + ); + expect(directUpdate.status, DavConditionalStatus.success); + + result = await mutations.update( + hrefKey: original.hrefKey, + uri: original.requestUri, + baselineEtag: original.etag!, + baselineRawIcs: original.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'BusyMax merged title'), + ], + ), + capabilities: collection.capabilities, + correlationId: 'live-event-disjoint-merge', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + current = result.canonicalObject!; + expect(current.rawIcsBody, contains('SUMMARY:BusyMax merged title')); + expect(current.rawIcsBody, contains('LOCATION:Nextcloud-side location')); + _expectEventPreservation(current.rawIcsBody!); + + final conflictBaseline = current; + final remoteSummaryPatch = DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'Nextcloud conflicting title'), + ], + ); + final conflictUpdate = await _mutationClient('events').conditionalPut( + uri: current.requestUri, + rawIcs: remoteSummaryPatch.applyTo( + current.rawIcsBody!, + nowUtc: DateTime.utc(2026, 8, 8, 12, 34, 56), + ), + correlationId: 'live-event-conflicting-out-of-band-update', + ifMatch: current.etag, + ); + expect(conflictUpdate.status, DavConditionalStatus.success); + result = await mutations.update( + hrefKey: conflictBaseline.hrefKey, + uri: conflictBaseline.requestUri, + baselineEtag: conflictBaseline.etag!, + baselineRawIcs: conflictBaseline.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.recurrenceMaster, + operations: [ + DavPatchOperation.setText('SUMMARY', 'BusyMax conflicting title'), + ], + ), + capabilities: collection.capabilities, + correlationId: 'live-event-explicit-conflict', + ); + expect(result.outcome, DavMutationOutcome.conflict); + expect( + result.conflictRemoteObject!.rawIcsBody, + contains('SUMMARY:Nextcloud conflicting title'), + ); + current = result.conflictRemoteObject!; + + page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-event-update-sync', + ); + expect( + page.changedMembers.map((member) => member.hrefKey), + contains(current.hrefKey), + ); + syncToken = page.nextSyncToken; + + for (final object in [current, ...variants]) { + final deletion = await mutations.delete( + hrefKey: object.hrefKey, + uri: object.requestUri, + baselineEtag: object.etag!, + baselineRawIcs: object.rawIcsBody!, + isEvent: true, + capabilities: collection.capabilities, + correlationId: 'live-event-delete', + ); + expect(deletion.outcome, DavMutationOutcome.succeeded); + } + page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-event-delete-sync', + ); + expect( + page.deletedHrefKeys, + containsAll([ + current.hrefKey, + ...variants.map((object) => object.hrefKey), + ]), + ); + + final invalidTokenError = await _captureDavError( + remote.syncCollectionPage( + syncToken: 'https://busymax.invalid/sync-token/does-not-exist', + correlationId: 'live-invalid-sync-token', + ), + ); + expect(invalidTokenError.kind, DavErrorKind.invalidSyncToken); + } + + Future verifyTransientReadRecovery(DavDiscoveryResult discovery) async { + for (final statusCode in const [429, 503, 500]) { + final injected = _InjectedStatusClient( + delegate: client, + statusCode: statusCode, + ); + final delays = []; + final retryingTransport = DavHttpTransport( + client: injected, + profile: profile, + accountAuthority: authority, + delay: (duration) async => delays.add(duration), + ); + final response = await retryingTransport.send( + DavRequest.xml( + method: 'PROPFIND', + uri: discovery.service.calendarHomeHref, + accountId: 'nextcloud-live', + correlationId: 'live-injected-retry-$statusCode', + headers: const {'depth': '0'}, + body: ''' +''', + ), + credential: credential, + ); + expect(response.statusCode, 207); + expect(injected.calls, 2); + expect(delays, hasLength(1)); + if (statusCode == 429 || statusCode == 503) { + expect(delays.single, Duration.zero); + } + } + } + + Future verifyTasks( + DavCollectionDiscovery collection, + String suffix, + ) async { + final remote = _collectionClient(collection, 'tasks'); + final mutations = _mutationService('tasks'); + var page = await remote.syncCollectionPage( + syncToken: '', + correlationId: 'live-tasks-empty-sync', + ); + expect(page.changedMembers, isEmpty); + var syncToken = page.nextSyncToken; + + final parentUid = 'busymax-live-parent-$suffix@example.invalid'; + final parentResult = await mutations.create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: parentUid, + initialMemberName: 'busymax-parent-$suffix.ics', + rawIcs: _parentTask(parentUid), + componentType: 'VTODO', + ), + capabilities: collection.capabilities, + correlationId: 'live-parent-task-create', + ); + expect(parentResult.outcome, DavMutationOutcome.succeeded); + var parent = parentResult.canonicalObject!; + + final childUid = 'busymax-live-child-$suffix@example.invalid'; + var result = await mutations.create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: childUid, + initialMemberName: 'busymax-child-$suffix.ics', + rawIcs: _childTask(childUid, parentUid), + componentType: 'VTODO', + ), + capabilities: collection.capabilities, + correlationId: 'live-child-task-create', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + var child = result.canonicalObject!; + _expectTaskPreservation(child.rawIcsBody!, parentUid); + + page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-task-create-sync', + ); + expect(page.changedMembers, hasLength(2)); + syncToken = page.nextSyncToken; + final fetched = await remote.fetchMembers( + page.changedMembers, + correlationId: 'live-task-multiget', + useCalendarMultiget: true, + ); + expect(fetched, hasLength(2)); + + for (final percent in const [55, 100, 0]) { + result = await mutations.update( + hrefKey: child.hrefKey, + uri: child.requestUri, + baselineEtag: child.etag!, + baselineRawIcs: child.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VTODO', uid: childUid), + scope: DavMutationScope.object, + operations: [DavPatchOperation.setTaskProgress(percent)], + ), + capabilities: collection.capabilities, + correlationId: 'live-task-progress-$percent', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + child = result.canonicalObject!; + final semantic = IcalSemanticDocument.parse( + child.rawIcsBody!, + ).components.single; + expect(semantic.percentComplete, percent); + if (percent == 100) { + expect(semantic.status, 'COMPLETED'); + expect(semantic.completed, isNotNull); + } else { + expect(semantic.completed, isNull); + } + _expectTaskPreservation(child.rawIcsBody!, parentUid); + } + + page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-task-update-sync', + ); + expect( + page.changedMembers.map((member) => member.hrefKey), + contains(child.hrefKey), + ); + syncToken = page.nextSyncToken; + + for (final entry in [(child, false), (parent, false)]) { + final deletion = await mutations.delete( + hrefKey: entry.$1.hrefKey, + uri: entry.$1.requestUri, + baselineEtag: entry.$1.etag!, + baselineRawIcs: entry.$1.rawIcsBody!, + isEvent: entry.$2, + capabilities: collection.capabilities, + correlationId: 'live-task-delete', + ); + expect(deletion.outcome, DavMutationOutcome.succeeded); + } + page = await remote.syncCollectionPage( + syncToken: syncToken, + correlationId: 'live-task-delete-sync', + ); + expect(page.deletedHrefKeys, containsAll([child.hrefKey, parent.hrefKey])); + } + + Future verifyLargeCollection( + DavCollectionDiscovery collection, + String suffix, { + required int memberCount, + }) async { + final remote = _collectionClient(collection, 'large'); + final mutationClient = _mutationClient('large'); + final emptyPage = await remote.syncCollectionPage( + syncToken: '', + correlationId: 'live-large-empty-sync', + ); + expect(emptyPage.changedMembers, isEmpty); + + final collectionBase = collection.requestUri.path.endsWith('/') + ? collection.requestUri + : collection.requestUri.replace(path: '${collection.requestUri.path}/'); + for (var index = 0; index < memberCount; index += 1) { + final response = await mutationClient.conditionalPut( + uri: collectionBase.resolve('busymax-large-$index-$suffix.ics'), + rawIcs: _simpleEvent( + uid: 'busymax-large-$index-$suffix@example.invalid', + summary: 'BusyMax large event $index', + startLine: + 'DTSTART:20260901T${(index % 20).toString().padLeft(2, '0')}0000Z', + endLine: + 'DTEND:20260901T${((index % 20) + 1).toString().padLeft(2, '0')}0000Z', + ), + correlationId: 'live-large-create-$index', + ifNoneMatch: true, + ); + expect(response.status, DavConditionalStatus.success); + } + + final page = await remote.syncCollectionPage( + syncToken: emptyPage.nextSyncToken, + correlationId: 'live-large-create-sync', + ); + expect(page.changedMembers, hasLength(memberCount)); + expect(page.truncated, isFalse); + final inventory = await remote.listMemberEtags( + correlationId: 'live-large-inventory', + ); + expect(inventory.members, hasLength(memberCount)); + + var fetchedCount = 0; + for (var offset = 0; offset < inventory.members.length; offset += 32) { + final end = offset + 32 < inventory.members.length + ? offset + 32 + : inventory.members.length; + final fetched = await remote.fetchMembers( + inventory.members.sublist(offset, end), + correlationId: 'live-large-multiget-$offset', + useCalendarMultiget: true, + ); + expect(fetched, everyElement(isA())); + fetchedCount += fetched.length; + } + expect(fetchedCount, memberCount); + + for (final member in inventory.members) { + final response = await mutationClient.conditionalDelete( + uri: member.requestUri, + ifMatch: member.etag, + correlationId: 'live-large-delete', + ); + expect(response.status, DavConditionalStatus.success); + } + final deletionPage = await remote.syncCollectionPage( + syncToken: page.nextSyncToken, + correlationId: 'live-large-delete-sync', + ); + expect(deletionPage.deletedHrefKeys, hasLength(memberCount)); + } + + Future verifyProductionSyncEngine( + DavCollectionDiscovery collection, + DavDiscoveryResult discovery, + ) async { + final database = AppDatabase(NativeDatabase.memory()); + try { + const seededAt = '2026-08-09T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: discovery.accountId, + provider: BusyProvider.nextcloud.storageValue, + authority: authority.toString(), + providerAccountId: credential.username, + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: seededAt, + updatedAtUtc: seededAt, + ), + ); + await DavDiscoveryRepository( + database: database, + ).commitSuccessfulInventory(discovery); + final storedCollection = await (database.select( + database.davCollections, + )..where((row) => row.hrefKey.equals(collection.hrefKey))).getSingle(); + var nextObjectId = 0; + final objectRepository = DavObjectRepository( + database: database, + idFactory: () => 'live-object-${nextObjectId += 1}', + ); + var notificationRebuilds = 0; + final delegate = _collectionClient(collection, 'production-engine'); + + final suffix = DateTime.now().microsecondsSinceEpoch.toString(); + await _createEngineEvent( + collection, + suffix: '$suffix-initial', + summary: 'BusyMax engine baseline', + ); + final initial = await DavSyncEngine( + database: database, + objectRepository: objectRepository, + remoteClient: delegate, + accountId: discovery.accountId, + collectionId: storedCollection.id, + provider: BusyProvider.nextcloud, + onNotificationsNeedRebuild: (_) async => notificationRebuilds += 1, + ).synchronize(correlationId: 'live-engine-initial'); + expect(initial.initialOrRebaseline, isTrue); + expect(initial.objectsFetched, 1); + expect( + await database.select(database.calendarEvents).get(), + hasLength(1), + ); + expect(notificationRebuilds, 1); + + final cursor = await database.select(database.syncCursors).getSingle(); + const invalidToken = + 'https://busymax.invalid/sync-token/full-engine-rebaseline'; + await (database.update(database.syncCursors) + ..where((row) => row.id.equals(cursor.id))) + .write(const SyncCursorsCompanion(cursorValue: Value(invalidToken))); + await _createEngineEvent( + collection, + suffix: '$suffix-rebaseline', + summary: 'BusyMax engine rebaseline', + ); + var observedSafeRebaseline = false; + final observingRemote = _ObservedLiveRemoteClient( + delegate: delegate, + beforeSync: (token) async { + if (token != '') return; + expect( + await database.select(database.davObjects).get(), + hasLength(1), + ); + expect( + (await database.select(database.syncCursors).getSingle()) + .cursorValue, + invalidToken, + ); + observedSafeRebaseline = true; + }, + ); + final rebaseline = await DavSyncEngine( + database: database, + objectRepository: objectRepository, + remoteClient: observingRemote, + accountId: discovery.accountId, + collectionId: storedCollection.id, + provider: BusyProvider.nextcloud, + onNotificationsNeedRebuild: (_) async => notificationRebuilds += 1, + ).synchronize(correlationId: 'live-engine-rebaseline'); + expect(observingRemote.requestedTokens.first, invalidToken); + expect(observingRemote.requestedTokens.last, ''); + expect(observedSafeRebaseline, isTrue); + expect(rebaseline.initialOrRebaseline, isTrue); + expect( + await database.select(database.calendarEvents).get(), + hasLength(2), + ); + expect(notificationRebuilds, 2); + + final committedCursor = + (await database.select(database.syncCursors).getSingle()).cursorValue; + await _createEngineEvent( + collection, + suffix: '$suffix-interrupted-a', + summary: 'BusyMax interrupted A', + ); + await _createEngineEvent( + collection, + suffix: '$suffix-interrupted-b', + summary: 'BusyMax interrupted B', + ); + final failingRemote = _ObservedLiveRemoteClient( + delegate: delegate, + failFetchCall: 2, + ); + await expectLater( + DavSyncEngine( + database: database, + objectRepository: objectRepository, + remoteClient: failingRemote, + accountId: discovery.accountId, + collectionId: storedCollection.id, + provider: BusyProvider.nextcloud, + limits: const DavSyncLimits(maximumMembersPerMultiget: 1), + ).synchronize(correlationId: 'live-engine-interrupted-fetch'), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'InjectedLiveLaterFetchFailure', + ), + ), + ); + expect(failingRemote.fetchCalls, 2); + expect( + await database.select(database.calendarEvents).get(), + hasLength(2), + ); + final preservedCursor = await database + .select(database.syncCursors) + .getSingle(); + expect(preservedCursor.cursorValue, committedCursor); + expect(preservedCursor.lastFailureCode, 'InjectedLiveLaterFetchFailure'); + expect(preservedCursor.inProgressGeneration, isNull); + + final remoteBeforeQueue = await delegate.listMemberEtags( + correlationId: 'live-offline-before-queue', + ); + var nextPendingId = 0; + final queuedUid = 'busymax-live-queued-$suffix@example.invalid'; + await DavPendingOperationQueue( + database: database, + idFactory: () => 'live-pending-${nextPendingId += 1}', + nowUtc: () => DateTime.utc(2026, 8, 9, 12), + ).enqueueCreate( + accountId: discovery.accountId, + collectionId: storedCollection.id, + object: DavNewObject( + uid: queuedUid, + initialMemberName: 'busymax-live-queued-$suffix.ics', + rawIcs: _simpleEvent( + uid: queuedUid, + summary: 'BusyMax queued offline', + startLine: 'DTSTART:20260904T160000Z', + endLine: 'DTEND:20260904T170000Z', + ), + componentType: 'VEVENT', + ), + ); + expect(await database.select(database.pendingOps).get(), hasLength(1)); + expect( + await database.select(database.calendarEvents).get(), + hasLength(2), + ); + expect( + (await delegate.listMemberEtags( + correlationId: 'live-offline-after-queue', + )).members.length, + remoteBeforeQueue.members.length, + ); + + final replayNotificationObjects = {}; + final replayFollowUpCollections = {}; + var nextReplayId = 0; + final replay = await DavPendingOperationsReplayer( + database: database, + accountId: discovery.accountId, + objectRepository: objectRepository, + serviceFactory: ({required account, required collection}) async => + _mutationService('live-offline-replay'), + rebuildNotifications: (ids) async => + replayNotificationObjects.addAll(ids), + requestFollowUpSync: (ids) async => + replayFollowUpCollections.addAll(ids), + idFactory: () => 'live-replay-${nextReplayId += 1}', + nowUtc: () => DateTime.utc(2026, 8, 9, 12), + ).replayDueOperations(); + expect(replay.appliedCount, 1); + expect(replay.paused, isFalse); + expect(await database.select(database.pendingOps).get(), isEmpty); + expect( + (await database.select(database.calendarEvents).get()).map( + (event) => event.title, + ), + contains('BusyMax queued offline'), + ); + expect(replayNotificationObjects, hasLength(1)); + expect(replayFollowUpCollections, {storedCollection.id}); + expect( + (await delegate.listMemberEtags( + correlationId: 'live-offline-after-replay', + )).members.length, + remoteBeforeQueue.members.length + 1, + ); + + final baselineEvent = + (await database.select(database.calendarEvents).get()).singleWhere( + (event) => event.title == 'BusyMax engine baseline', + ); + await DavPendingOperationQueue( + database: database, + idFactory: () => 'live-revoked-pending', + nowUtc: () => DateTime.utc(2026, 8, 9, 12), + ).enqueueUpdate( + accountId: discovery.accountId, + collectionId: storedCollection.id, + objectId: baselineEvent.davObjectId!, + patch: DavMutationPatch( + target: IcalComponentKey( + componentType: 'VEVENT', + uid: baselineEvent.icalUid!, + ), + scope: DavMutationScope.object, + operations: [ + DavPatchOperation.setText('SUMMARY', 'Must remain queued'), + ], + ), + ); + final rejectedCredential = DavBasicCredential( + username: credential.username, + password: '${credential.password}-revoked', + ); + final rejectedReplay = await DavPendingOperationsReplayer( + database: database, + accountId: discovery.accountId, + objectRepository: objectRepository, + serviceFactory: ({required account, required collection}) async => + DavConditionalMutationService( + remoteClient: DavMutationHttpClient( + transport: transport, + accountId: discovery.accountId, + collectionId: collection.id, + credential: rejectedCredential, + ), + ), + idFactory: () => 'live-rejected-replay', + nowUtc: () => DateTime.utc(2026, 8, 9, 12), + ).replayDueOperations(); + expect(rejectedReplay.paused, isTrue); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'reauth_required', + ); + final authBlocked = await database + .select(database.pendingOps) + .getSingle(); + expect(authBlocked.state, 'auth_blocked'); + expect(authBlocked.lastErrorCode, isNotNull); + expect(await database.select(database.davObjects).get(), hasLength(3)); + expect( + (await database.select(database.calendarEvents).get()).map( + (event) => event.title, + ), + isNot(contains('Must remain queued')), + ); + } finally { + await database.close(); + } + } + + Future _createEngineEvent( + DavCollectionDiscovery collection, { + required String suffix, + required String summary, + }) async { + final uid = 'busymax-live-engine-$suffix@example.invalid'; + final result = await _mutationService('production-engine').create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: uid, + initialMemberName: 'busymax-engine-$suffix.ics', + rawIcs: _simpleEvent( + uid: uid, + summary: summary, + startLine: 'DTSTART:20260903T160000Z', + endLine: 'DTEND:20260903T170000Z', + ), + componentType: 'VEVENT', + ), + capabilities: collection.capabilities, + correlationId: 'live-engine-create', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + } + + Future prepareRestartObject( + DavCollectionDiscovery collection, + String restartId, + ) async { + final remote = _collectionClient(collection, 'restart'); + final emptyPage = await remote.syncCollectionPage( + syncToken: '', + correlationId: 'live-restart-empty-sync', + ); + expect(emptyPage.changedMembers, isEmpty); + final uid = '$restartId@example.invalid'; + final result = await _mutationService('restart').create( + collectionUri: collection.requestUri, + object: DavNewObject( + uid: uid, + initialMemberName: '$restartId.ics', + rawIcs: _simpleEvent( + uid: uid, + summary: 'BusyMax before restart', + startLine: 'DTSTART:20260902T160000Z', + endLine: 'DTEND:20260902T170000Z', + ), + componentType: 'VEVENT', + ), + capabilities: collection.capabilities, + correlationId: 'live-restart-create', + ); + expect(result.outcome, DavMutationOutcome.succeeded); + final page = await remote.syncCollectionPage( + syncToken: emptyPage.nextSyncToken, + correlationId: 'live-restart-create-sync', + ); + expect(page.changedMembers, hasLength(1)); + } + + Future verifyRestartObject( + DavCollectionDiscovery collection, + String restartId, + ) async { + final remote = _collectionClient(collection, 'restart'); + final page = await remote.syncCollectionPage( + syncToken: '', + correlationId: 'live-restart-post-restart-sync', + ); + expect(page.changedMembers, hasLength(1)); + final fetched = await remote.fetchMembers( + page.changedMembers, + correlationId: 'live-restart-post-restart-fetch', + useCalendarMultiget: true, + ); + var current = fetched.single; + expect(current.rawIcsBody, contains('SUMMARY:BusyMax before restart')); + final uid = '$restartId@example.invalid'; + final update = await _mutationService('restart').update( + hrefKey: current.hrefKey, + uri: current.requestUri, + baselineEtag: current.etag!, + baselineRawIcs: current.rawIcsBody!, + patch: DavMutationPatch( + target: IcalComponentKey(componentType: 'VEVENT', uid: uid), + scope: DavMutationScope.object, + operations: [ + DavPatchOperation.setText('SUMMARY', 'BusyMax after restart'), + ], + ), + capabilities: collection.capabilities, + correlationId: 'live-restart-update', + ); + expect(update.outcome, DavMutationOutcome.succeeded); + current = update.canonicalObject!; + expect(current.rawIcsBody, contains('SUMMARY:BusyMax after restart')); + final deletion = await _mutationService('restart').delete( + hrefKey: current.hrefKey, + uri: current.requestUri, + baselineEtag: current.etag!, + baselineRawIcs: current.rawIcsBody!, + isEvent: true, + capabilities: collection.capabilities, + correlationId: 'live-restart-delete', + ); + expect(deletion.outcome, DavMutationOutcome.succeeded); + } + + Future deleteCollection(Uri uri) async { + final response = await transport.send( + DavRequest( + method: 'DELETE', + uri: uri, + accountId: 'nextcloud-live', + correlationId: 'live-restart-delete-collection', + ), + credential: credential, + ); + expect(response.statusCode, 204); + } + + DavCollectionHttpClient _collectionClient( + DavCollectionDiscovery collection, + String id, + ) => DavCollectionHttpClient( + transport: transport, + profile: profile, + accountAuthority: authority, + accountId: 'nextcloud-live', + collectionId: id, + collectionUri: collection.requestUri, + credential: credential, + ); + + DavMutationHttpClient _mutationClient(String id) => DavMutationHttpClient( + transport: transport, + accountId: 'nextcloud-live', + collectionId: id, + credential: credential, + ); + + DavConditionalMutationService _mutationService(String id) => + DavConditionalMutationService( + remoteClient: _mutationClient(id), + nowUtc: () => DateTime.utc(2026, 8, 8, 12, 34, 56), + ); + + Future close() async { + for (final uri in _collectionsToDelete.reversed) { + try { + await transport.send( + DavRequest( + method: 'DELETE', + uri: uri, + accountId: 'nextcloud-live', + correlationId: 'live-cleanup', + ), + credential: credential, + ); + } on Object { + // Cleanup is best effort; the unique QA-only collection can be removed + // with `occ dav:delete-calendar` after diagnosing a failed run. + } + } + client.close(); + } +} + +final class _ObservedLiveRemoteClient implements DavCollectionRemoteClient { + _ObservedLiveRemoteClient({ + required this.delegate, + this.beforeSync, + this.failFetchCall, + }); + + final DavCollectionRemoteClient delegate; + final Future Function(String syncToken)? beforeSync; + final int? failFetchCall; + final List requestedTokens = []; + int fetchCalls = 0; + + @override + Future syncCollectionPage({ + required String syncToken, + required String correlationId, + DavCancellationToken? cancellationToken, + }) async { + requestedTokens.add(syncToken); + await beforeSync?.call(syncToken); + return delegate.syncCollectionPage( + syncToken: syncToken, + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + } + + @override + Future listMemberEtags({ + required String correlationId, + DavCancellationToken? cancellationToken, + }) => delegate.listMemberEtags( + correlationId: correlationId, + cancellationToken: cancellationToken, + ); + + @override + Future> fetchMembers( + List members, { + required String correlationId, + required bool useCalendarMultiget, + DavCancellationToken? cancellationToken, + }) { + fetchCalls += 1; + if (fetchCalls == failFetchCall) { + throw const DavException( + kind: DavErrorKind.server, + code: 'InjectedLiveLaterFetchFailure', + safeMessage: 'Injected integration-test failure.', + ); + } + return delegate.fetchMembers( + members, + correlationId: correlationId, + useCalendarMultiget: useCalendarMultiget, + cancellationToken: cancellationToken, + ); + } +} + +final class _InjectedStatusClient extends http.BaseClient { + _InjectedStatusClient({required this.delegate, required this.statusCode}); + + final http.Client delegate; + final int statusCode; + int calls = 0; + + @override + Future send(http.BaseRequest request) { + calls += 1; + if (calls == 1) { + return Future.value( + http.StreamedResponse( + const Stream>.empty(), + statusCode, + headers: statusCode == 429 || statusCode == 503 + ? const {'retry-after': '0'} + : const {}, + request: request, + ), + ); + } + return delegate.send(request); + } + + @override + void close() { + // The fixture owns the shared delegate. + } +} + +DavCollectionDiscovery _collectionBySlug( + DavDiscoveryResult discovery, + String slug, +) => discovery.collections.singleWhere( + (collection) => collection.requestUri.pathSegments.contains(slug), +); + +Uri _collectionChild(Uri home, String slug) { + final basePath = home.path.endsWith('/') ? home.path : '${home.path}/'; + return home.replace(path: '$basePath$slug/', query: null, fragment: null); +} + +Future _captureDavError(Future operation) async { + try { + await operation; + } on DavException catch (error) { + return error; + } + throw TestFailure('Expected a DavException.'); +} + +void _expectEventPreservation(String rawIcs) { + expect(rawIcs, contains('RRULE:FREQ=WEEKLY;COUNT=3')); + expect(rawIcs, contains('EXDATE;TZID=America/Vancouver:20260816T090000')); + expect(rawIcs, contains('RECURRENCE-ID;TZID=America/Vancouver')); + expect(rawIcs, contains('X-BUSYMAX-QA;X-PARAM="alpha,beta":opaque')); + expect('BEGIN:VALARM'.allMatches(rawIcs), hasLength(2)); + expect(rawIcs, contains('ACTION:AUDIO')); + expect(rawIcs, contains('STATUS:CANCELLED')); +} + +void _expectTaskPreservation(String rawIcs, String parentUid) { + expect( + IcalSemanticDocument.parse(rawIcs).components.single.parentUid, + parentUid, + ); + expect(rawIcs, contains('X-APPLE-SORT-ORDER:42')); + expect(rawIcs, contains('X-OC-HIDESUBTASKS:1')); + expect(rawIcs, contains('RRULE:FREQ=DAILY;COUNT=2')); + expect(rawIcs, contains('X-BUSYMAX-TASK-QA:opaque')); + expect(rawIcs, contains('BEGIN:VALARM')); +} + +String _recurringEvent(String uid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//Nextcloud Integration Test//EN\r +BEGIN:VTIMEZONE\r +TZID:America/Vancouver\r +BEGIN:STANDARD\r +DTSTART:20251102T020000\r +TZOFFSETFROM:-0700\r +TZOFFSETTO:-0800\r +TZNAME:PST\r +END:STANDARD\r +BEGIN:DAYLIGHT\r +DTSTART:20260308T020000\r +TZOFFSETFROM:-0800\r +TZOFFSETTO:-0700\r +TZNAME:PDT\r +END:DAYLIGHT\r +END:VTIMEZONE\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +DTSTART;TZID=America/Vancouver:20260809T090000\r +DTEND;TZID=America/Vancouver:20260809T100000\r +RRULE:FREQ=WEEKLY;COUNT=3\r +EXDATE;TZID=America/Vancouver:20260816T090000\r +SUMMARY:BusyMax live event\r +CATEGORIES:BusyMax,CalDAV\r +X-BUSYMAX-QA;X-PARAM="alpha,beta":opaque\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT15M\r +DESCRIPTION:Visible reminder\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +ATTACH:Glass\r +X-ALARM-QA:opaque\r +END:VALARM\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +RECURRENCE-ID;TZID=America/Vancouver:20260823T090000\r +DTSTART;TZID=America/Vancouver:20260823T110000\r +DTEND;TZID=America/Vancouver:20260823T120000\r +SUMMARY:BusyMax moved exception\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +RECURRENCE-ID;TZID=America/Vancouver:20260830T090000\r +DTSTART;TZID=America/Vancouver:20260830T090000\r +DTEND;TZID=America/Vancouver:20260830T100000\r +STATUS:CANCELLED\r +SUMMARY:BusyMax cancelled exception\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _simpleEvent({ + required String uid, + required String summary, + required String startLine, + required String endLine, +}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//Nextcloud Integration Test//EN\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +$startLine\r +$endLine\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _parentTask(String uid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//Nextcloud Integration Test//EN\r +BEGIN:VTODO\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +DTSTART:20260809T160000Z\r +DUE:20260809T170000Z\r +SUMMARY:BusyMax parent task\r +STATUS:NEEDS-ACTION\r +PERCENT-COMPLETE:0\r +PRIORITY:3\r +CATEGORIES:BusyMax,Parent\r +END:VTODO\r +END:VCALENDAR\r +'''; + +String _childTask(String uid, String parentUid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//Nextcloud Integration Test//EN\r +BEGIN:VTODO\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +DTSTART;VALUE=DATE:20260810\r +DUE;VALUE=DATE:20260811\r +SUMMARY:BusyMax child task\r +STATUS:NEEDS-ACTION\r +PERCENT-COMPLETE:0\r +PRIORITY:5\r +CATEGORIES:BusyMax,Child\r +RELATED-TO;RELTYPE=PARENT:$parentUid\r +RRULE:FREQ=DAILY;COUNT=2\r +X-APPLE-SORT-ORDER:42\r +X-OC-HIDESUBTASKS:1\r +X-BUSYMAX-TASK-QA:opaque\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT30M\r +DESCRIPTION:Task reminder\r +END:VALARM\r +END:VTODO\r +END:VCALENDAR\r +'''; + +String _requiredEnvironment(String name) { + final value = Platform.environment[name]?.trim(); + if (value == null || value.isEmpty) { + throw StateError('$name is required when $_enabledVariable=1.'); + } + return value; +} diff --git a/test/dav/nextcloud_login_flow_live_test.dart b/test/dav/nextcloud_login_flow_live_test.dart new file mode 100644 index 0000000..8075e69 --- /dev/null +++ b/test/dav/nextcloud_login_flow_live_test.dart @@ -0,0 +1,468 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/auth/nextcloud_app_password_revoker.dart'; +import 'package:busymax/src/dav/auth/nextcloud_login_flow_v2.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_service.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +const _enabledVariable = 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE'; + +void main() { + final enabled = Platform.environment[_enabledVariable] == '1'; + + test( + 'real browser Login Flow returns and revokes a canonical app password', + () async { + final server = Uri.parse( + _requiredEnvironment('BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL'), + ); + final username = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE_USERNAME', + ); + final bootstrapAppPassword = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE_APP_PASSWORD', + ); + final certificatePath = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT', + ); + expect(server.scheme, 'https'); + + final securityContext = SecurityContext(withTrustedRoots: false) + ..setTrustedCertificates(certificatePath); + final flowHttpClient = HttpClient(context: securityContext); + final flowClient = IOClient(flowHttpClient); + addTearDown(() { + flowClient.close(); + flowHttpClient.close(force: true); + }); + final browser = await _HeadlessLoginFlowBrowser.start( + username: username, + appPassword: bootstrapAppPassword, + ); + addTearDown(browser.close); + + final flow = NextcloudLoginFlowV2( + client: flowClient, + browserLauncher: browser.authorize, + pollInterval: const Duration(milliseconds: 100), + operationTimeout: const Duration(minutes: 2), + ); + final result = await flow.start(server.toString()); + expect(result.canonicalServer, server); + expect(result.loginName, username); + expect(result.appPassword, bootstrapAppPassword); + expect(result.toString(), isNot(contains(username))); + expect(result.toString(), isNot(contains(bootstrapAppPassword))); + + final profile = davProviderProfile( + BusyProvider.nextcloud, + nextcloudServer: result.canonicalServer, + ); + final davHttpClient = HttpClient(context: securityContext); + final davClient = IOClient(davHttpClient); + addTearDown(() { + davClient.close(); + davHttpClient.close(force: true); + }); + final transport = DavHttpTransport( + client: davClient, + profile: profile, + accountAuthority: result.canonicalServer, + ); + final credential = DavBasicCredential( + username: result.loginName, + password: result.appPassword, + ); + final discovery = DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: result.canonicalServer, + accountId: 'nextcloud-login-live', + credential: credential, + ); + final discovered = await discovery.discover( + correlationId: 'nextcloud-login-live-discovery', + ); + expect(discovered.service.calendarHomeHref.path, isNotEmpty); + + await NextcloudAppPasswordRevoker(transport: transport).revoke( + accountId: 'nextcloud-login-live', + credential: NextcloudSecretRecord( + canonicalServer: result.canonicalServer, + loginName: result.loginName, + appPassword: result.appPassword, + ), + correlationId: 'nextcloud-login-live-revoke', + ); + await expectLater( + discovery.discover( + correlationId: 'nextcloud-login-live-revoked-discovery', + ), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.authentication, + ), + ), + ); + }, + skip: enabled + ? false + : 'Set $_enabledVariable=1 and its URL, username, temporary app ' + 'password, and TLS certificate variables to run this test.', + timeout: const Timeout(Duration(minutes: 3)), + ); + + test( + 'real Login Flow start can be cancelled while browser authorization opens', + () async { + final server = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL'); + final certificatePath = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT', + ); + final securityContext = SecurityContext(withTrustedRoots: false) + ..setTrustedCertificates(certificatePath); + final ioClient = HttpClient(context: securityContext); + final client = IOClient(ioClient); + addTearDown(() { + client.close(); + ioClient.close(force: true); + }); + late final NextcloudLoginFlowV2 flow; + flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async { + flow.cancel(); + return true; + }, + ); + + await expectLater( + flow.start(server), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.cancelled, + ), + ), + ); + }, + skip: enabled ? false : 'Live Nextcloud Login Flow is not enabled.', + timeout: const Timeout(Duration(minutes: 1)), + ); + + test( + 'real pending polling remains 404 until the operation expires', + () async { + final server = _requiredEnvironment('BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL'); + final certificatePath = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT', + ); + final securityContext = SecurityContext(withTrustedRoots: false) + ..setTrustedCertificates(certificatePath); + final ioClient = HttpClient(context: securityContext); + final client = IOClient(ioClient); + addTearDown(() { + client.close(); + ioClient.close(force: true); + }); + var clock = DateTime.utc(2026, 8, 8, 12); + var pendingDelays = 0; + final flow = NextcloudLoginFlowV2( + client: client, + browserLauncher: (_) async => true, + nowUtc: () => clock, + operationTimeout: const Duration(seconds: 2), + pollInterval: const Duration(seconds: 1), + delay: (duration) async { + pendingDelays += 1; + clock = clock.add(duration); + }, + ); + + await expectLater( + flow.start(server), + throwsA( + isA() + .having((error) => error.kind, 'kind', DavErrorKind.timeout) + .having( + (error) => error.code, + 'code', + 'NextcloudLoginFlowExpired', + ), + ), + ); + expect(pendingDelays, 2); + }, + skip: enabled ? false : 'Live Nextcloud Login Flow is not enabled.', + timeout: const Timeout(Duration(minutes: 1)), + ); +} + +final class _HeadlessLoginFlowBrowser { + _HeadlessLoginFlowBrowser({ + required this.username, + required this.appPassword, + required this.process, + required this.profileDirectory, + required _ChromeDevTools devTools, + }) : _devTools = devTools; + + static Future<_HeadlessLoginFlowBrowser> start({ + required String username, + required String appPassword, + }) async { + final debugReservation = await ServerSocket.bind( + InternetAddress.loopbackIPv4, + 0, + ); + final debugPort = debugReservation.port; + await debugReservation.close(); + final profileDirectory = await Directory.systemTemp.createTemp( + 'busymax-nextcloud-login-browser-', + ); + final executable = + Platform.environment['BUSYMAX_NEXTCLOUD_LOGIN_LIVE_BROWSER'] ?? + 'google-chrome'; + final process = await Process.start(executable, [ + '--headless=new', + '--disable-gpu', + '--disable-background-networking', + '--disable-component-update', + '--disable-breakpad', + '--disable-crash-reporter', + '--disable-default-apps', + '--disable-sync', + '--ignore-certificate-errors', + '--no-first-run', + '--no-default-browser-check', + '--remote-debugging-address=127.0.0.1', + '--remote-debugging-port=$debugPort', + '--user-data-dir=${profileDirectory.path}', + 'about:blank', + ]); + unawaited(process.stdout.drain()); + unawaited(process.stderr.drain()); + try { + final webSocketUri = await _waitForDevTools(debugPort); + final devTools = await _ChromeDevTools.connect(webSocketUri); + return _HeadlessLoginFlowBrowser( + username: username, + appPassword: appPassword, + process: process, + profileDirectory: profileDirectory, + devTools: devTools, + ); + } on Object { + process.kill(); + await process.exitCode; + await _deleteDirectoryWithRetries(profileDirectory); + rethrow; + } + } + + final String username; + final String appPassword; + final Process process; + final Directory profileDirectory; + final _ChromeDevTools _devTools; + + Future authorize(Uri loginUri) async { + final target = await _devTools.call( + 'Target.createTarget', + parameters: const {'url': 'about:blank'}, + ); + final attached = await _devTools.call( + 'Target.attachToTarget', + parameters: {'targetId': target['targetId'], 'flatten': true}, + ); + final sessionId = attached['sessionId']! as String; + await _devTools.call('Page.enable', sessionId: sessionId); + await _devTools.call( + 'Page.navigate', + parameters: {'url': loginUri.toString()}, + sessionId: sessionId, + ); + + await _waitForBrowserCondition(() async { + final value = await _devTools.evaluate('''(() => { + const candidate = [...document.querySelectorAll('button, a')] + .find((element) => element.textContent.includes('Alternative log in using app password')); + if (!candidate) return false; + candidate.click(); + return true; + })()''', sessionId: sessionId); + return value == true; + }); + + await _waitForBrowserCondition(() async { + final value = await _devTools.evaluate('''(() => { + const user = document.querySelector('input[name="user"]'); + const password = document.querySelector('input[name="password"]'); + const form = user?.closest('form'); + if (!user || !password || !form) return false; + const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; + setValue.call(user, ${jsonEncode(username)}); + setValue.call(password, ${jsonEncode(appPassword)}); + user.dispatchEvent(new Event('input', { bubbles: true })); + password.dispatchEvent(new Event('input', { bubbles: true })); + form.requestSubmit(); + return true; + })()''', sessionId: sessionId); + return value == true; + }); + return true; + } + + Future close() async { + await _devTools.close(); + process.kill(); + await process.exitCode; + await _deleteDirectoryWithRetries(profileDirectory); + } +} + +final class _ChromeDevTools { + _ChromeDevTools._(this._socket) { + _socket.listen( + _receive, + onError: _failAll, + onDone: () => _failAll(StateError('Chrome DevTools disconnected.')), + ); + } + + static Future<_ChromeDevTools> connect(Uri uri) async => + _ChromeDevTools._(await WebSocket.connect(uri.toString())); + + final WebSocket _socket; + final Map>> _pending = {}; + var _nextId = 1; + + Future> call( + String method, { + Map parameters = const {}, + String? sessionId, + }) { + final id = _nextId++; + final completer = Completer>(); + _pending[id] = completer; + _socket.add( + jsonEncode({ + 'id': id, + 'method': method, + 'params': parameters, + if (sessionId != null) 'sessionId': sessionId, + }), + ); + return completer.future.timeout(const Duration(seconds: 15)); + } + + Future evaluate( + String expression, { + required String sessionId, + }) async { + final result = await call( + 'Runtime.evaluate', + parameters: { + 'expression': expression, + 'returnByValue': true, + 'awaitPromise': true, + }, + sessionId: sessionId, + ); + final remote = (result['result']! as Map).cast(); + return remote['value']; + } + + void _receive(Object? rawMessage) { + if (rawMessage is! String) return; + final decoded = jsonDecode(rawMessage); + if (decoded is! Map) return; + final message = decoded.cast(); + final id = message['id']; + if (id is! int) return; + final completer = _pending.remove(id); + if (completer == null) return; + if (message['error'] case final Map error) { + completer.completeError( + StateError('Chrome DevTools command failed: ${error['code']}.'), + ); + return; + } + completer.complete( + ((message['result'] as Map?) ?? const {}).cast(), + ); + } + + void _failAll(Object error) { + for (final completer in _pending.values) { + if (!completer.isCompleted) completer.completeError(error); + } + _pending.clear(); + } + + Future close() => _socket.close(); +} + +Future _waitForDevTools(int port) async { + final client = http.Client(); + try { + for (var attempt = 0; attempt < 100; attempt += 1) { + try { + final response = await client + .get(Uri.parse('http://127.0.0.1:$port/json/version')) + .timeout(const Duration(seconds: 1)); + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as Map; + return Uri.parse(json['webSocketDebuggerUrl']! as String); + } + } on Object { + // Chrome is still starting. + } + await Future.delayed(const Duration(milliseconds: 100)); + } + } finally { + client.close(); + } + throw StateError('Headless Chrome did not expose DevTools in time.'); +} + +Future _waitForBrowserCondition(Future Function() condition) async { + for (var attempt = 0; attempt < 300; attempt += 1) { + if (await condition()) return; + await Future.delayed(const Duration(milliseconds: 100)); + } + throw StateError('The Nextcloud browser authorization UI did not load.'); +} + +Future _deleteDirectoryWithRetries(Directory directory) async { + for (var attempt = 0; attempt < 50; attempt += 1) { + if (!await directory.exists()) return; + try { + await directory.delete(recursive: true); + return; + } on FileSystemException { + if (attempt == 49) rethrow; + await Future.delayed(const Duration(milliseconds: 100)); + } + } +} + +String _requiredEnvironment(String name) { + final value = Platform.environment[name]?.trim(); + if (value == null || value.isEmpty) { + throw StateError('$name is required when $_enabledVariable=1.'); + } + return value; +} diff --git a/test/dav/nextcloud_sharing_live_test.dart b/test/dav/nextcloud_sharing_live_test.dart new file mode 100644 index 0000000..4bfa045 --- /dev/null +++ b/test/dav/nextcloud_sharing_live_test.dart @@ -0,0 +1,364 @@ +import 'dart:io'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_models.dart'; +import 'package:busymax/src/dav/discovery/dav_discovery_service.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/dav/mutation/dav_conditional_mutation_service.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +const _enabledVariable = 'BUSYMAX_NEXTCLOUD_SHARING_LIVE'; + +void main() { + final enabled = Platform.environment[_enabledVariable] == '1'; + + test( + 'Nextcloud user and group shares enforce live ACL changes', + () async { + final authority = Uri.parse( + _requiredEnvironment('BUSYMAX_NEXTCLOUD_SHARING_LIVE_URL'), + ); + final owner = _SharingUser.fromEnvironment(authority, 'OWNER'); + final writer = _SharingUser.fromEnvironment(authority, 'WRITER'); + final reader = _SharingUser.fromEnvironment(authority, 'READER'); + final groupMember = _SharingUser.fromEnvironment( + authority, + 'GROUP_MEMBER', + ); + final groupName = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP', + ); + addTearDown(owner.close); + addTearDown(writer.close); + addTearDown(reader.close); + addTearDown(groupMember.close); + + final ownerDiscovery = await owner.discover('sharing-owner-discovery'); + final suffix = DateTime.now().microsecondsSinceEpoch.toString(); + final slug = 'busymax-qa-shared-$suffix'; + final displayName = 'BusyMax QA Shared $suffix'; + final collectionUri = _collectionChild( + ownerDiscovery.service.calendarHomeHref, + slug, + ); + await owner.createEventCollection( + collectionUri, + displayName: displayName, + ); + var collectionDeleted = false; + addTearDown(() async { + if (!collectionDeleted) await owner.deleteCollection(collectionUri); + }); + + await owner.updateShares( + collectionUri, + set: [ + _ShareGrant.user(writer.username, writable: true), + _ShareGrant.user(reader.username, writable: false), + _ShareGrant.group(groupName, writable: false), + ], + ); + + var writerCollection = _sharedCollection( + await writer.discover('sharing-writer-discovery'), + displayName, + ); + var readerCollection = _sharedCollection( + await reader.discover('sharing-reader-discovery'), + displayName, + ); + final groupCollection = _sharedCollection( + await groupMember.discover('sharing-group-discovery'), + displayName, + ); + expect(writerCollection.capabilities.canCreateEvent, isTrue); + expect(writerCollection.capabilities.isReadOnly, isFalse); + expect(readerCollection.capabilities.canCreateEvent, isFalse); + expect(readerCollection.capabilities.isReadOnly, isTrue); + expect(groupCollection.capabilities.canCreateEvent, isFalse); + expect(groupCollection.capabilities.isReadOnly, isTrue); + + final eventUid = 'busymax-shared-$suffix@example.invalid'; + final writerMutations = writer.mutations('shared-writer'); + final created = await writerMutations.create( + collectionUri: writerCollection.requestUri, + object: DavNewObject( + uid: eventUid, + initialMemberName: 'busymax-shared-$suffix.ics', + rawIcs: _event(eventUid), + componentType: 'VEVENT', + ), + capabilities: writerCollection.capabilities, + correlationId: 'sharing-writer-create', + ); + expect(created.outcome, DavMutationOutcome.succeeded); + + await expectLater( + reader + .mutations('shared-reader') + .create( + collectionUri: readerCollection.requestUri, + object: DavNewObject( + uid: 'busymax-readonly-$suffix@example.invalid', + initialMemberName: 'busymax-readonly-$suffix.ics', + rawIcs: _event('busymax-readonly-$suffix@example.invalid'), + componentType: 'VEVENT', + ), + capabilities: readerCollection.capabilities, + correlationId: 'sharing-reader-rejected-create', + ), + throwsA( + isA() + .having((error) => error.kind, 'kind', DavErrorKind.authorization) + .having((error) => error.code, 'code', 'DavReadOnly'), + ), + ); + + await owner.updateShares( + collectionUri, + set: [_ShareGrant.user(writer.username, writable: false)], + remove: [_ShareGrant.user(reader.username, writable: false)], + ); + writerCollection = _sharedCollection( + await writer.discover('sharing-writer-downgraded'), + displayName, + ); + expect(writerCollection.capabilities.isReadOnly, isTrue); + expect(writerCollection.capabilities.canCreateEvent, isFalse); + expect( + _hasSharedCollection( + await reader.discover('sharing-reader-removed'), + displayName, + ), + isFalse, + ); + + await owner.deleteCollection(collectionUri); + collectionDeleted = true; + expect( + _hasSharedCollection( + await writer.discover('sharing-collection-removed'), + displayName, + ), + isFalse, + ); + expect( + _hasSharedCollection( + await groupMember.discover('sharing-group-collection-removed'), + displayName, + ), + isFalse, + ); + }, + skip: enabled + ? false + : 'Set $_enabledVariable=1 and disposable user/group environment ' + 'variables to run the live Nextcloud sharing test.', + timeout: const Timeout(Duration(minutes: 3)), + ); +} + +final class _SharingUser { + _SharingUser({ + required this.authority, + required this.username, + required this.credential, + required this.client, + required this.profile, + required this.transport, + }); + + factory _SharingUser.fromEnvironment(Uri authority, String role) { + final username = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_SHARING_LIVE_${role}_USERNAME', + ); + final password = _requiredEnvironment( + 'BUSYMAX_NEXTCLOUD_SHARING_LIVE_${role}_PASSWORD', + ); + final profile = DavProviderProfile( + provider: BusyProvider.nextcloud, + bootstrapUri: authority, + calendarEnabled: true, + tasksEnabled: true, + allowCollectionMutations: false, + allowSchedulingMutations: false, + allowMove: false, + allowInsecureLoopbackForTesting: + authority.scheme == 'http' && + const {'127.0.0.1', 'localhost', '::1'}.contains(authority.host), + ); + final client = http.Client(); + return _SharingUser( + authority: authority, + username: username, + credential: DavBasicCredential(username: username, password: password), + client: client, + profile: profile, + transport: DavHttpTransport( + client: client, + profile: profile, + accountAuthority: authority, + ), + ); + } + + final Uri authority; + final String username; + final DavBasicCredential credential; + final http.Client client; + final DavProviderProfile profile; + final DavHttpTransport transport; + + Future discover(String correlationId) => + DavDiscoveryService( + transport: transport, + profile: profile, + accountAuthority: authority, + accountId: 'sharing-$username', + credential: credential, + ).discover(correlationId: correlationId); + + DavConditionalMutationService mutations(String collectionId) => + DavConditionalMutationService( + remoteClient: DavMutationHttpClient( + transport: transport, + accountId: 'sharing-$username', + collectionId: collectionId, + credential: credential, + ), + ); + + Future createEventCollection( + Uri collectionUri, { + required String displayName, + }) async { + final response = await transport.send( + DavRequest.xml( + method: 'MKCALENDAR', + uri: collectionUri, + accountId: 'sharing-$username', + correlationId: 'sharing-create-collection', + retryClass: DavRetryClass.never, + body: + ''' + + + + ${_xmlText(displayName)} + + +''', + ), + credential: credential, + ); + expect(response.statusCode, 201); + } + + Future updateShares( + Uri collectionUri, { + List<_ShareGrant> set = const [], + List<_ShareGrant> remove = const [], + }) async { + final response = await transport.send( + DavRequest.xml( + method: 'POST', + uri: collectionUri, + accountId: 'sharing-$username', + correlationId: 'sharing-update-acl', + retryClass: DavRetryClass.never, + body: + ''' + + ${set.map((grant) => grant.setXml).join()} + ${remove.map((grant) => grant.removeXml).join()} +''', + ), + credential: credential, + ); + expect(response.statusCode, 200); + } + + Future deleteCollection(Uri collectionUri) async { + final response = await transport.send( + DavRequest( + method: 'DELETE', + uri: collectionUri, + accountId: 'sharing-$username', + correlationId: 'sharing-delete-collection', + ), + credential: credential, + ); + expect(response.statusCode, anyOf(204, 404)); + } + + void close() => client.close(); +} + +final class _ShareGrant { + const _ShareGrant._(this.href, this.writable); + + factory _ShareGrant.user(String username, {required bool writable}) => + _ShareGrant._('principal:principals/users/$username', writable); + + factory _ShareGrant.group(String group, {required bool writable}) => + _ShareGrant._('principal:principals/groups/$group', writable); + + final String href; + final bool writable; + + String get setXml => + '${_xmlText(href)}' + '${writable ? '' : ''}'; + + String get removeXml => + '${_xmlText(href)}'; +} + +DavCollectionDiscovery _sharedCollection( + DavDiscoveryResult discovery, + String displayName, +) => discovery.collections.singleWhere( + (collection) => collection.displayName.startsWith(displayName), +); + +bool _hasSharedCollection(DavDiscoveryResult discovery, String displayName) => + discovery.collections.any( + (collection) => collection.displayName.startsWith(displayName), + ); + +Uri _collectionChild(Uri home, String slug) { + final basePath = home.path.endsWith('/') ? home.path : '${home.path}/'; + return home.replace(path: '$basePath$slug/', query: null, fragment: null); +} + +String _event(String uid) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax//Nextcloud Sharing Integration Test//EN\r +BEGIN:VEVENT\r +UID:$uid\r +DTSTAMP:20260808T120000Z\r +DTSTART:20260812T160000Z\r +DTEND:20260812T170000Z\r +SUMMARY:BusyMax shared event\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _xmlText(String value) => value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + +String _requiredEnvironment(String name) { + final value = Platform.environment[name]?.trim(); + if (value == null || value.isEmpty) { + throw StateError('$name is required when $_enabledVariable=1.'); + } + return value; +} diff --git a/test/dav/storage/dav_object_repository_test.dart b/test/dav/storage/dav_object_repository_test.dart new file mode 100644 index 0000000..867a205 --- /dev/null +++ b/test/dav/storage/dav_object_repository_test.dart @@ -0,0 +1,506 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late AppDatabase database; + late DavObjectRepository repository; + var nextId = 0; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + repository = DavObjectRepository( + database: database, + idFactory: () => 'generated-${nextId += 1}', + ); + await _seedCollection(database); + }); + + tearDown(() => database.close()); + + test( + 'stores exact raw event and atomically builds recurrence projections', + () async { + final raw = _recurringEvent(summary: 'Original', unknown: 'keep-me'); + final prepared = DavPreparedObject.parse( + hrefKey: '/remote.php/dav/calendars/alex/work/event.ics', + requestUri: Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/work/event.ics', + ), + etag: 'W/"opaque-etag"', + contentType: 'text/calendar; charset=utf-8', + rawIcsBody: raw, + ); + + await repository.commit( + _commit( + objects: [prepared], + membership: {prepared.hrefKey}, + cursor: 'https://cloud.example.test/sync/1', + ), + ); + + final object = await database.select(database.davObjects).getSingle(); + expect(object.rawIcsBody, raw); + expect(object.etag, 'W/"opaque-etag"'); + expect(object.rawIcsBody, contains('X-UNKNOWN:keep-me')); + expect(object.serverDeleted, isFalse); + expect( + await database.select(database.davObjectComponents).get(), + hasLength(2), + ); + final events = await database.select(database.calendarEvents).get(); + expect(events, hasLength(3)); + expect(events.map((event) => event.title), contains('Moved')); + expect(events.every((event) => event.davObjectId == object.id), isTrue); + expect( + events.every((event) => event.startTimeZone == 'America/Vancouver'), + isTrue, + ); + expect( + events.every( + (event) => + event.remindersJson?.contains('"minutes":[10,30]') ?? false, + ), + isTrue, + ); + expect(events.first.remindersJson, contains('AUDIO')); + final cursor = await database.select(database.syncCursors).getSingle(); + expect(cursor.cursorValue, 'https://cloud.example.test/sync/1'); + expect(cursor.baselineGeneration, 1); + }, + ); + + test( + 'server-only folding changes replace raw baseline without data loss', + () async { + final first = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"one"', + contentType: 'text/calendar', + rawIcsBody: _simpleEvent('A long summary that can be folded'), + ); + await repository.commit( + _commit(objects: [first], membership: {_eventHref}, cursor: 'token-1'), + ); + final originalId = + (await database.select(database.davObjects).getSingle()).id; + final rewrittenBody = _simpleEvent('A long summary that can be folded') + .replaceFirst( + 'SUMMARY:A long summary that can be folded', + 'SUMMARY:A long summary that can be\r\n folded', + ); + final rewritten = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"two"', + contentType: 'text/calendar', + rawIcsBody: rewrittenBody, + ); + + await repository.commit( + _commit( + objects: [rewritten], + membership: {_eventHref}, + cursor: 'token-2', + generation: 2, + ), + ); + + final object = await database.select(database.davObjects).getSingle(); + expect(object.id, originalId); + expect(object.rawIcsBody, rewrittenBody); + expect(object.etag, '"two"'); + expect(object.semanticHash, first.semantic.semanticHash); + expect( + await database.select(database.calendarEvents).get(), + hasLength(1), + ); + }, + ); + + test('complete membership deletes only after successful promotion', () async { + final first = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"one"', + contentType: 'text/calendar', + rawIcsBody: _simpleEvent('Existing'), + ); + await repository.commit( + _commit(objects: [first], membership: {_eventHref}, cursor: 'token-1'), + ); + + await repository.commit( + _commit( + objects: const [], + membership: const {}, + cursor: 'token-2', + generation: 2, + ), + ); + + final object = await database.select(database.davObjects).getSingle(); + expect(object.serverDeleted, isTrue); + expect(object.rawIcsBody, first.rawIcsBody); + expect(await database.select(database.calendarEvents).get(), isEmpty); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'token-2', + ); + }); + + test( + 'projection failure rolls raw object and final cursor back together', + () async { + final first = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"one"', + contentType: 'text/calendar', + rawIcsBody: _simpleEvent('Baseline'), + ); + await repository.commit( + _commit(objects: [first], membership: {_eventHref}, cursor: 'token-1'), + ); + final changed = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"two"', + contentType: 'text/calendar', + rawIcsBody: _simpleEvent('Changed'), + ); + + await expectLater( + repository.commit( + _commit( + objects: [changed], + membership: {_eventHref}, + cursor: 'token-2', + generation: 2, + rangeEnd: DateTime.utc(2055), + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'IcalProjectionRangeLimitExceeded', + ), + ), + ); + + final object = await database.select(database.davObjects).getSingle(); + expect(object.rawIcsBody, first.rawIcsBody); + expect(object.etag, '"one"'); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'token-1', + ); + }, + ); + + test('projects Nextcloud task semantics and extension fields', () async { + final task = DavPreparedObject.parse( + hrefKey: '/remote.php/dav/calendars/alex/work/task.ics', + requestUri: Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/work/task.ics', + ), + etag: '"task-etag"', + contentType: 'text/calendar', + rawIcsBody: _task, + ); + + await repository.commit( + _commit(objects: [task], membership: {task.hrefKey}, cursor: 'token-1'), + ); + + final projected = await database.select(database.tasks).getSingle(); + expect(projected.title, 'Child task'); + expect(projected.parentUid, 'parent-uid'); + // Keep the wire UID even when the referenced parent is not present, but do + // not expose an unresolved UID through the projection-ID relationship. + expect(projected.parent, null); + expect(projected.icalPriority, 1); + expect(projected.percentComplete, 50); + expect(projected.sortOrder, 42); + expect(projected.position, '42'); + expect(projected.providerStatus, 'IN-PROCESS'); + expect(projected.status, 'inProcess'); + expect(projected.taskLocation, 'Room 4'); + expect(projected.taskUrl, 'https://cloud.example.test/tasks/child'); + expect(projected.taskClassification, 'PRIVATE'); + expect(projected.taskPinned, isTrue); + expect(projected.taskHideSubtasks, isTrue); + expect(projected.taskHideCompletedSubtasks, isTrue); + expect(jsonDecode(projected.categoriesJson!), ['Work', 'Personal']); + expect(projected.providerExtensionProjectionJson, contains('X-PINNED')); + expect(projected.providerMetadataJson, contains('nativeDue')); + expect(projected.providerMetadataJson, contains('nativeStart')); + expect(projected.microsoftIsReminderOn, isTrue); + expect(projected.microsoftReminderDateTime, '2026-08-09T23:00:00.000Z'); + expect(projected.microsoftReminderTimeZone, 'UTC'); + expect(projected.providerMetadataJson, contains('AUDIO')); + + final entity = TaskEntity.fromRow(projected); + expect(entity.microsoftStartDateTime, '2026-08-09T16:00:00Z'); + expect(entity.microsoftStartTimeZone, 'UTC'); + expect(entity.microsoftDueDateTime, '2026-08-09T17:00:00'); + expect(entity.microsoftDueTimeZone, 'America/Vancouver'); + }); + + test('uses the Nextcloud CREATED fallback for task sort order', () async { + final task = DavPreparedObject.parse( + hrefKey: '/remote.php/dav/calendars/alex/work/created-order.ics', + requestUri: Uri.parse( + 'https://cloud.example.test/remote.php/dav/calendars/alex/work/created-order.ics', + ), + etag: '"created-order"', + contentType: 'text/calendar', + rawIcsBody: _taskWithoutExplicitOrder, + ); + + await repository.commit( + _commit(objects: [task], membership: {task.hrefKey}, cursor: 'token-1'), + ); + + final projected = await database.select(database.tasks).getSingle(); + final expected = DateTime.utc( + 2026, + 8, + 9, + 12, + ).difference(DateTime.utc(2001, 1, 1)).inSeconds; + expect(projected.sortOrder, expected); + expect(projected.position, '$expected'); + }); + + test('advances occurrence horizon from stored raw data only', () async { + final future = DavPreparedObject.parse( + hrefKey: _eventHref, + requestUri: Uri.parse('https://cloud.example.test$_eventHref'), + etag: '"future"', + contentType: 'text/calendar', + rawIcsBody: _simpleEvent( + 'Future', + start: '20300101T090000Z', + end: '20300101T100000Z', + ), + ); + await repository.commit( + _commit(objects: [future], membership: {_eventHref}, cursor: 'token-1'), + ); + expect(await database.select(database.davObjects).get(), hasLength(1)); + expect(await database.select(database.calendarEvents).get(), isEmpty); + + await repository.reprojectCollectionFromStored( + accountId: 'account', + collectionId: 'collection', + provider: BusyProvider.nextcloud, + projectionRangeStartUtc: DateTime.utc(2029), + projectionRangeEndUtc: DateTime.utc(2031), + completedAtUtc: _now, + ); + + expect(await database.select(database.calendarEvents).get(), hasLength(1)); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'token-1', + ); + }); +} + +const _eventHref = '/remote.php/dav/calendars/alex/work/event.ics'; +final _now = DateTime.utc(2026, 8, 8, 12); + +DavCollectionCommit _commit({ + required List objects, + required Set membership, + required String cursor, + int generation = 1, + DateTime? rangeEnd, +}) => DavCollectionCommit( + accountId: 'account', + collectionId: 'collection', + provider: BusyProvider.nextcloud, + objects: objects, + deletedHrefKeys: const {}, + completeMembership: true, + membershipHrefKeys: membership, + finalCursorKind: 'dav_sync_token', + finalCursorValue: cursor, + baselineGeneration: generation, + completedAtUtc: _now, + projectionRangeStartUtc: DateTime.utc(2025), + projectionRangeEndUtc: rangeEnd ?? DateTime.utc(2029), +); + +Future _seedCollection(AppDatabase database) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + const href = '/remote.php/dav/calendars/alex/work/'; + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: href, + requestUri: 'https://cloud.example.test$href', + displayName: 'Work', + supportedComponentMask: const Value(3), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + color: const Value('#123456'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-collection', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: href, + davCollectionId: const Value('collection'), + summary: 'Work', + createdAtLocal: _now.millisecondsSinceEpoch, + updatedAtLocal: _now.millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-collection', + davCollectionId: const Value('collection'), + title: 'Work', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} + +String _simpleEvent( + String summary, { + String start = '20260808T090000Z', + String end = '20260808T100000Z', +}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:simple@example.test\r +DTSTART:$start\r +DTEND:$end\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _recurringEvent({required String summary, required String unknown}) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:recurring@example.test\r +DTSTART;TZID=America/Vancouver:20260803T090000\r +DTEND;TZID=America/Vancouver:20260803T100000\r +RRULE:FREQ=WEEKLY;COUNT=3\r +SUMMARY:$summary\r +X-UNKNOWN:$unknown\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT10M\r +DESCRIPTION:First\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER:-PT30M\r +DESCRIPTION:Second\r +END:VALARM\r +END:VEVENT\r +BEGIN:VEVENT\r +UID:recurring@example.test\r +RECURRENCE-ID;TZID=America/Vancouver:20260810T090000\r +DTSTART;TZID=America/Vancouver:20260810T110000\r +DTEND;TZID=America/Vancouver:20260810T120000\r +SUMMARY:Moved\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +const _task = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//Nextcloud Tasks//EN\r +BEGIN:VTODO\r +UID:child-uid\r +SUMMARY:Child task\r +DESCRIPTION:Details\r +DTSTART:20260809T160000Z\r +DUE;TZID=America/Vancouver:20260809T170000\r +STATUS:IN-PROCESS\r +PERCENT-COMPLETE:50\r +PRIORITY:1\r +CATEGORIES:Work,Personal\r +LOCATION:Room 4\r +URL:https://cloud.example.test/tasks/child\r +CLASS:PRIVATE\r +RELATED-TO:parent-uid\r +X-APPLE-SORT-ORDER:42\r +X-PINNED:true\r +X-OC-HIDESUBTASKS:1\r +X-OC-HIDECOMPLETEDSUBTASKS:1\r +BEGIN:VALARM\r +ACTION:DISPLAY\r +TRIGGER;VALUE=DATE-TIME:20260809T230000Z\r +DESCRIPTION:Task reminder\r +END:VALARM\r +BEGIN:VALARM\r +ACTION:AUDIO\r +TRIGGER:-PT5M\r +END:VALARM\r +END:VTODO\r +END:VCALENDAR\r +'''; + +const _taskWithoutExplicitOrder = '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//Nextcloud Tasks//EN\r +BEGIN:VTODO\r +UID:created-order@example.test\r +CREATED:20260809T120000Z\r +SUMMARY:Created order\r +END:VTODO\r +END:VCALENDAR\r +'''; diff --git a/test/dav/support/fake_dav_server.dart b/test/dav/support/fake_dav_server.dart new file mode 100644 index 0000000..d42d1f4 --- /dev/null +++ b/test/dav/support/fake_dav_server.dart @@ -0,0 +1,632 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:http/http.dart' as http; + +final class FakeDavResource { + FakeDavResource({ + required this.path, + required this.body, + required this.etag, + this.multistatusStatus, + }); + + final String path; + String body; + String etag; + int? multistatusStatus; +} + +final class FakeDavFault { + const FakeDavFault({ + this.method, + this.path, + this.statusCode, + this.body = '', + this.headers = const {}, + this.delay = Duration.zero, + this.dropConnection = false, + }); + + final String? method; + final String? path; + final int? statusCode; + final String body; + final Map headers; + final Duration delay; + final bool dropConnection; + + bool matches(HttpRequest request) => + (method == null || method == request.method) && + (path == null || path == request.uri.path); +} + +final class FakeDavRequestRecord { + const FakeDavRequestRecord({ + required this.method, + required this.path, + required this.depth, + required this.ifMatch, + required this.ifNoneMatch, + required this.hasBasicAuthorization, + required this.body, + }); + + final String method; + final String path; + final String? depth; + final String? ifMatch; + final String? ifNoneMatch; + final bool hasBasicAuthorization; + final String body; +} + +/// A loopback-only, stateful DAV origin used by integration tests. It models +/// discovery, inventory, RFC 6578 paging, multiget, conditional writes, +/// server rewriting, ACL changes, and injected protocol/transport failures. +final class FakeDavServer { + FakeDavServer({this.installationPath = '/nextcloud'}); + + final String installationPath; + HttpServer? _server; + final List _faults = []; + final List _clients = []; + final Map resources = {}; + final List requests = []; + final Set deletedPaths = {}; + final Set invalidSyncTokens = {'invalid-token'}; + + bool redirectWellKnown = true; + bool collectionReadOnly = false; + bool collectionRemoved = false; + bool paginateInitialSync = true; + bool rewriteMutations = false; + bool raceNextMutation = false; + bool dropAfterNextMutation = false; + String finalSyncToken = 'sync-token-1'; + int _revision = 10; + + String get davRootPath => '$installationPath/remote.php/dav/'; + String get principalPath => '${davRootPath}principals/users/alex/'; + String get calendarHomePath => '${davRootPath}calendars/alex/'; + String get collectionPath => '${calendarHomePath}work/'; + String get eventPath => '${collectionPath}event.ics'; + String get taskPath => '${collectionPath}task.ics'; + + Uri get authority { + final server = _server; + if (server == null) throw StateError('Fake DAV server is not running.'); + return Uri( + scheme: 'http', + host: InternetAddress.loopbackIPv4.address, + port: server.port, + path: installationPath, + ); + } + + Uri uriFor(String path) => authority.replace(path: path); + + DavProviderProfile get profile => DavProviderProfile( + provider: BusyProvider.nextcloud, + bootstrapUri: authority, + calendarEnabled: true, + tasksEnabled: true, + allowCollectionMutations: false, + allowSchedulingMutations: false, + allowMove: false, + allowInsecureLoopbackForTesting: true, + ); + + DavBasicCredential get credential => + DavBasicCredential(username: 'alex', password: 'app-password'); + + Future start() async { + if (_server != null) return; + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + _server = server; + resources + ..clear() + ..[eventPath] = FakeDavResource( + path: eventPath, + etag: '"event-1"', + body: _eventBody('Server event'), + ) + ..[taskPath] = FakeDavResource( + path: taskPath, + etag: 'W/"task-1"', + body: _taskBody('Server task'), + ); + unawaited( + server.forEach(_handle).catchError((Object _) { + // A deliberately dropped test connection can surface at the server + // listener after the client has already observed the network error. + }), + ); + } + + Future close() async { + final server = _server; + _server = null; + for (final client in _clients) { + client.close(); + } + _clients.clear(); + await server?.close(force: true); + } + + void enqueueFault(FakeDavFault fault) => _faults.add(fault); + + DavHttpTransport transport({ + DavTransportLimits limits = const DavTransportLimits(), + DavDelay? delay, + }) { + final client = http.Client(); + _clients.add(client); + return DavHttpTransport( + client: client, + profile: profile, + accountAuthority: authority, + limits: limits, + delay: delay ?? (_) async {}, + ); + } + + Future _handle(HttpRequest request) async { + final bytes = []; + await for (final chunk in request) { + bytes.addAll(chunk); + } + final body = utf8.decode(bytes, allowMalformed: true); + requests.add( + FakeDavRequestRecord( + method: request.method, + path: request.uri.path, + depth: request.headers.value('depth'), + ifMatch: request.headers.value(HttpHeaders.ifMatchHeader), + ifNoneMatch: request.headers.value(HttpHeaders.ifNoneMatchHeader), + hasBasicAuthorization: + request.headers + .value(HttpHeaders.authorizationHeader) + ?.startsWith('Basic ') == + true, + body: body, + ), + ); + + final faultIndex = _faults.indexWhere((fault) => fault.matches(request)); + if (faultIndex >= 0) { + final fault = _faults.removeAt(faultIndex); + if (fault.delay > Duration.zero) await Future.delayed(fault.delay); + if (fault.dropConnection) { + await _drop(request); + return; + } + await _respond( + request, + fault.statusCode ?? HttpStatus.internalServerError, + fault.body, + headers: fault.headers, + ); + return; + } + + if (!requests.last.hasBasicAuthorization) { + await _respond(request, HttpStatus.unauthorized, ''); + return; + } + final path = request.uri.path; + if (path == '$installationPath/.well-known/caldav') { + if (redirectWellKnown) { + await _respond( + request, + HttpStatus.movedPermanently, + '', + headers: {'location': uriFor(davRootPath).toString()}, + ); + } else { + await _options(request); + } + return; + } + if (request.method == 'OPTIONS' && path == davRootPath) { + await _options(request); + return; + } + if (request.method == 'PROPFIND' && path == davRootPath) { + await _xml(request, HttpStatus.multiStatus, _principalResponse()); + return; + } + if (request.method == 'PROPFIND' && path == principalPath) { + await _xml(request, HttpStatus.multiStatus, _homeResponse()); + return; + } + if (request.method == 'PROPFIND' && path == calendarHomePath) { + await _xml(request, HttpStatus.multiStatus, _inventoryResponse()); + return; + } + if (path == collectionPath && collectionRemoved) { + await _respond(request, HttpStatus.notFound, ''); + return; + } + if (request.method == 'PROPFIND' && path == collectionPath) { + await _xml(request, HttpStatus.multiStatus, _memberInventoryResponse()); + return; + } + if (request.method == 'REPORT' && path == collectionPath) { + if (body.contains('sync-collection')) { + await _syncCollection(request, body); + } else if (body.contains('calendar-multiget')) { + await _calendarMultiget(request, body); + } else { + await _respond(request, HttpStatus.badRequest, ''); + } + return; + } + if (request.method == 'GET') { + await _getResource(request, path); + return; + } + if (request.method == 'PUT') { + await _putResource(request, path, body); + return; + } + if (request.method == 'DELETE') { + await _deleteResource(request, path); + return; + } + await _respond(request, HttpStatus.notFound, ''); + } + + Future _options(HttpRequest request) => _respond( + request, + HttpStatus.ok, + '', + headers: { + 'dav': '1, 3, calendar-access, sync-collection', + 'allow': 'OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE', + }, + ); + + Future _syncCollection(HttpRequest request, String body) async { + final token = _syncToken(body); + if (invalidSyncTokens.contains(token)) { + await _xml( + request, + HttpStatus.forbidden, + '', + ); + return; + } + final live = resources.values + .where((resource) => resource.multistatusStatus != 404) + .toList(growable: false); + if (token.isEmpty && paginateInitialSync && live.length > 1) { + await _xml( + request, + HttpStatus.insufficientStorage, + _syncResponse( + token: 'intermediate-token', + live: [live.first], + deleted: const [], + ), + ); + return; + } + final page = token == 'intermediate-token' + ? live.skip(1).toList(growable: false) + : token.isEmpty + ? live + : const []; + await _xml( + request, + HttpStatus.multiStatus, + _syncResponse( + token: token == finalSyncToken + ? '$finalSyncToken-next' + : finalSyncToken, + live: page, + deleted: deletedPaths, + ), + ); + } + + Future _calendarMultiget(HttpRequest request, String body) async { + final requested = RegExp( + r'<(?:[A-Za-z0-9_-]+:)?href[^>]*>(.*?)', + dotAll: true, + ).allMatches(body).map((match) => _xmlDecode(match.group(1)!)).toSet(); + final responses = []; + for (final path in requested) { + final resource = resources[path]; + if (resource == null || deletedPaths.contains(path)) { + responses.add(_statusResponse(path, HttpStatus.notFound)); + } else if (resource.multistatusStatus case final status?) { + responses.add(_statusResponse(path, status)); + } else { + responses.add(_resourceResponse(resource, includeBody: true)); + } + } + await _xml(request, HttpStatus.multiStatus, _multistatus(responses)); + } + + Future _getResource(HttpRequest request, String path) async { + final resource = resources[path]; + if (resource == null || deletedPaths.contains(path)) { + await _respond(request, HttpStatus.notFound, ''); + return; + } + await _respond( + request, + HttpStatus.ok, + resource.body, + headers: { + 'etag': resource.etag, + 'content-type': 'text/calendar; charset=utf-8', + }, + ); + } + + Future _putResource( + HttpRequest request, + String path, + String body, + ) async { + if (collectionReadOnly) { + await _respond(request, HttpStatus.forbidden, ''); + return; + } + var current = resources[path]; + if (raceNextMutation && current != null) { + raceNextMutation = false; + current.etag = '"race-${_revision += 1}"'; + } + final ifNoneMatch = request.headers.value(HttpHeaders.ifNoneMatchHeader); + final ifMatch = request.headers.value(HttpHeaders.ifMatchHeader); + if (ifNoneMatch == '*' && current != null) { + await _respond(request, HttpStatus.preconditionFailed, ''); + return; + } + if (ifMatch != null && (current == null || current.etag != ifMatch)) { + await _respond(request, HttpStatus.preconditionFailed, ''); + return; + } + final canonical = rewriteMutations ? _rewrite(body) : body; + final created = current == null; + current ??= FakeDavResource(path: path, body: canonical, etag: ''); + current + ..body = canonical + ..etag = '"revision-${_revision += 1}"'; + resources[path] = current; + deletedPaths.remove(path); + if (dropAfterNextMutation) { + dropAfterNextMutation = false; + await _drop(request); + return; + } + await _respond( + request, + created ? HttpStatus.created : HttpStatus.noContent, + '', + headers: {'etag': current.etag}, + ); + } + + Future _deleteResource(HttpRequest request, String path) async { + if (collectionReadOnly) { + await _respond(request, HttpStatus.forbidden, ''); + return; + } + var current = resources[path]; + if (raceNextMutation && current != null) { + raceNextMutation = false; + current.etag = '"race-${_revision += 1}"'; + } + final ifMatch = request.headers.value(HttpHeaders.ifMatchHeader); + if (current == null || deletedPaths.contains(path)) { + await _respond(request, HttpStatus.notFound, ''); + return; + } + if (ifMatch == null || current.etag != ifMatch) { + await _respond(request, HttpStatus.preconditionFailed, ''); + return; + } + resources.remove(path); + deletedPaths.add(path); + if (dropAfterNextMutation) { + dropAfterNextMutation = false; + await _drop(request); + return; + } + await _respond(request, HttpStatus.noContent, ''); + } + + String _principalResponse() => _multistatus([ + '''${_xmlEscape(davRootPath)} +${_xmlEscape(principalPath)} +HTTP/1.1 200 OK''', + ]); + + String _homeResponse() => _multistatus([ + '''${_xmlEscape(principalPath)} +${_xmlEscape(calendarHomePath)} +mailto:alex@example.test +HTTP/1.1 200 OK''', + ]); + + String _inventoryResponse() { + final responses = [ + '''${_xmlEscape(calendarHomePath)} + +HTTP/1.1 200 OK''', + ]; + if (!collectionRemoved) { + final privileges = collectionReadOnly + ? '' + : ''' + +'''; + responses.add( + '''${_xmlEscape(collectionPath)} + +Work & Tasks +$privileges + + + + + +${_xmlEscape(finalSyncToken)} + + +HTTP/1.1 200 OK''', + ); + } + return _multistatus(responses); + } + + String _memberInventoryResponse() => _multistatus([ + '''${_xmlEscape(collectionPath)} + +HTTP/1.1 200 OK''', + for (final resource in resources.values) + if (!deletedPaths.contains(resource.path)) + _resourceOrStatusResponse(resource, includeBody: false), + ]); + + String _syncResponse({ + required String token, + required Iterable live, + required Iterable deleted, + }) => _multistatus([ + for (final resource in live) + _resourceOrStatusResponse(resource, includeBody: false), + for (final path in deleted) _statusResponse(path, HttpStatus.notFound), + ], syncToken: token); + + String _resourceResponse( + FakeDavResource resource, { + required bool includeBody, + }) => + '''${_xmlEscape(resource.path)} +${_xmlEscape(resource.etag)} +${includeBody ? '${_xmlEscape(resource.body)}' : ''} +HTTP/1.1 200 OK'''; + + String _resourceOrStatusResponse( + FakeDavResource resource, { + required bool includeBody, + }) { + final status = resource.multistatusStatus; + return status == null + ? _resourceResponse(resource, includeBody: includeBody) + : _statusResponse(resource.path, status); + } + + String _statusResponse(String path, int status) => + '${_xmlEscape(path)}' + 'HTTP/1.1 $status ${_reason(status)}'; + + String _multistatus(Iterable responses, {String? syncToken}) => + ''' + +${responses.join('\n')} +${syncToken == null ? '' : '${_xmlEscape(syncToken)}'} +'''; + + String _syncToken(String body) { + final match = RegExp( + r'<(?:[A-Za-z0-9_-]+:)?sync-token[^>]*>(.*?)', + dotAll: true, + ).firstMatch(body); + return match == null ? '' : _xmlDecode(match.group(1)!).trim(); + } + + String _rewrite(String body) { + if (body.contains('X-SERVER-REWRITE:canonical')) return body; + for (final end in const ['END:VEVENT', 'END:VTODO']) { + if (body.contains(end)) { + return body.replaceFirst(end, 'X-SERVER-REWRITE:canonical\r\n$end'); + } + } + return body; + } + + Future _xml(HttpRequest request, int status, String body) => _respond( + request, + status, + body, + headers: {'content-type': 'application/xml; charset=utf-8'}, + ); + + Future _respond( + HttpRequest request, + int status, + String body, { + Map headers = const {}, + }) async { + request.response.statusCode = status; + headers.forEach(request.response.headers.set); + if (body.isNotEmpty) request.response.add(utf8.encode(body)); + await request.response.close(); + } + + Future _drop(HttpRequest request) async { + final socket = await request.response.detachSocket(writeHeaders: false); + socket.destroy(); + } +} + +String _xmlEscape(String value) => + const HtmlEscape(HtmlEscapeMode.element).convert(value); + +String _xmlDecode(String value) => value + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('&', '&'); + +String _reason(int status) => switch (status) { + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 409 => 'Conflict', + 412 => 'Precondition Failed', + 423 => 'Locked', + 429 => 'Too Many Requests', + 500 => 'Internal Server Error', + 503 => 'Service Unavailable', + 507 => 'Insufficient Storage', + _ => 'Status', +}; + +String _eventBody(String summary) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Fake DAV//EN\r +BEGIN:VEVENT\r +UID:event@example.test\r +DTSTAMP:20260808T120000Z\r +DTSTART:20260809T090000Z\r +DTEND:20260809T100000Z\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +String _taskBody(String summary) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Fake DAV//EN\r +BEGIN:VTODO\r +UID:task@example.test\r +DTSTAMP:20260808T120000Z\r +DUE:20260809T120000Z\r +SUMMARY:$summary\r +END:VTODO\r +END:VCALENDAR\r +'''; diff --git a/test/dav/sync/dav_account_sync_engine_test.dart b/test/dav/sync/dav_account_sync_engine_test.dart new file mode 100644 index 0000000..327601b --- /dev/null +++ b/test/dav/sync/dav_account_sync_engine_test.dart @@ -0,0 +1,417 @@ +import 'dart:convert'; + +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/dav_provider_profile.dart'; +import 'package:busymax/src/dav/ical/ical_document.dart'; +import 'package:busymax/src/dav/mutation/dav_mutation_patch.dart'; +import 'package:busymax/src/dav/mutation/dav_pending_operations.dart'; +import 'package:busymax/src/dav/sync/dav_account_sync_engine.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +void main() { + late AppDatabase database; + late InMemorySecretStore secrets; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + secrets = InMemorySecretStore(); + await _seed(database, secrets); + }); + + tearDown(() => database.close()); + + test( + 'account sequence syncs, conditionally replays, follows up, then pauses on 401', + () async { + var syncReports = 0; + var unauthorized = false; + String serverBody = _event('Initial'); + var serverEtag = '"v1"'; + final requests = []; + final client = MockClient((request) async { + requests.add(request); + expect(request.headers['authorization'], startsWith('Basic ')); + if (unauthorized) return http.Response('', 401); + if (request.method == 'REPORT' && + request.body.contains('sync-collection')) { + syncReports += 1; + if (syncReports == 1) { + expect(request.body, contains('')); + return http.Response( + _syncResponse(token: 'token-1', etag: serverEtag), + 207, + ); + } + if (syncReports == 2) { + expect( + request.body, + contains('token-1'), + ); + return http.Response(_syncResponse(token: 'token-2'), 207); + } + expect( + request.body, + contains('token-2'), + ); + return http.Response( + _syncResponse(token: 'token-3', etag: serverEtag), + 207, + ); + } + if (request.method == 'REPORT' && + request.body.contains('calendar-multiget')) { + return http.Response(_multigetResponse(serverBody, serverEtag), 207); + } + if (request.method == 'PUT') { + expect(request.headers['if-match'], '"v1"'); + serverBody = request.body; + serverEtag = '"v2"'; + return http.Response('', 204); + } + if (request.method == 'GET') { + return http.Response( + serverBody, + 200, + headers: {'etag': serverEtag, 'content-type': 'text/calendar'}, + ); + } + fail('Unexpected ${request.method} ${request.url}'); + }); + final notificationObjects = {}; + DavAccountSyncEngine engine() => DavAccountSyncEngine( + database: database, + secretStore: secrets, + httpClient: client, + accountId: 'account', + policy: const DavAccountSyncPolicy( + discoveryMaxAge: Duration(days: 30), + inventoryMaxAge: Duration(days: 30), + ), + correlationIdFactory: () => 'safe-correlation', + nowUtc: () => _now, + rebuildNotifications: (accountId, ids) async { + expect(accountId, 'account'); + notificationObjects.addAll(ids); + }, + ); + + final initial = await engine().synchronize(); + expect(initial.collectionsSynchronized, 1); + expect(initial.discoveryRefreshed, isFalse); + final raw = await database.select(database.davObjects).getSingle(); + expect(raw.rawIcsBody, serverBody); + expect(raw.etag, '"v1"'); + expect( + (await database.select(database.calendarEvents).getSingle()).title, + 'Initial', + ); + expect(notificationObjects, {raw.id}); + + await DavPendingOperationQueue( + database: database, + idFactory: () => 'pending-update', + nowUtc: () => _now, + ).enqueueUpdate( + accountId: 'account', + collectionId: 'collection', + objectId: raw.id, + patch: DavMutationPatch( + target: const IcalComponentKey( + componentType: 'VEVENT', + uid: 'event@example.test', + ), + scope: DavMutationScope.object, + operations: [DavPatchOperation.setText('SUMMARY', 'Offline edit')], + ), + ); + notificationObjects.clear(); + + final replayed = await engine().synchronize(); + expect(replayed.pendingOperationsApplied, 1); + expect(replayed.followUpCollectionsSynchronized, 1); + expect(await database.select(database.pendingOps).get(), isEmpty); + expect(serverBody, contains('SUMMARY:Offline edit')); + expect( + (await database.select(database.calendarEvents).getSingle()).title, + 'Offline edit', + ); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'token-3', + ); + expect(notificationObjects, isNotEmpty); + + unauthorized = true; + await expectLater( + engine().synchronize(), + throwsA( + isA().having( + (error) => error.failures.single.kind, + 'failure kind', + DavErrorKind.authentication, + ), + ), + ); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'reauth_required', + ); + expect(await database.select(database.davObjects).get(), hasLength(1)); + expect( + (await database.select(database.calendarEvents).getSingle()).title, + 'Offline edit', + ); + expect( + requests.every((request) => !request.url.toString().contains('secret')), + isTrue, + ); + }, + ); + + test( + 'missing credential marks reauthentication without deleting cache', + () async { + await secrets.deleteCredential('account'); + final engine = DavAccountSyncEngine( + database: database, + secretStore: secrets, + httpClient: MockClient((_) async => http.Response('', 500)), + accountId: 'account', + ); + + await expectLater( + engine.synchronize(), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavCredentialsRevoked', + ), + ), + ); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'reauth_required', + ); + expect( + await database.select(database.davCollections).get(), + hasLength(1), + ); + }, + ); + + test('credential mismatch maps to reauthentication-required', () async { + await secrets.saveCredential( + 'account', + AppleICloudSecretRecord( + username: 'someone@example.test', + appSpecificPassword: 'not-the-nextcloud-secret', + ), + ); + final engine = DavAccountSyncEngine( + database: database, + secretStore: secrets, + httpClient: MockClient((_) async => http.Response('', 500)), + accountId: 'account', + ); + + await expectLater( + engine.synchronize(), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavCredentialsRevoked', + ), + ), + ); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'reauth_required', + ); + expect(await database.select(database.davCollections).get(), hasLength(1)); + }); + + test('locked credential store is temporary and preserves cache', () async { + final engine = DavAccountSyncEngine( + database: database, + secretStore: _UnavailableSecretStore(), + httpClient: MockClient((_) async => http.Response('', 500)), + accountId: 'account', + ); + + await expectLater( + engine.synchronize(), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavCredentialStoreUnavailable', + ), + ), + ); + expect( + (await database.select(database.accounts).getSingle()).authState, + 'temporarily_unavailable', + ); + expect(await database.select(database.davCollections).get(), hasLength(1)); + }); +} + +final class _UnavailableSecretStore extends InMemorySecretStore { + @override + Future readCredential(String accountId) { + throw const SecretStoreException( + 'SecretStoreUnavailable', + secretStorageUnavailableMessage, + ); + } +} + +const _collectionHref = '/cloud/remote.php/dav/calendars/alex/work/'; +const _eventHref = '${_collectionHref}event.ics'; +final _now = DateTime.utc(2026, 8, 8, 12); + +Future _seed(AppDatabase database, InMemorySecretStore secrets) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test/cloud', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await secrets.saveCredential( + 'account', + NextcloudSecretRecord( + canonicalServer: Uri.parse('https://cloud.example.test/cloud'), + loginName: 'alex', + appPassword: 'secret-app-password', + ), + ); + await database + .into(database.davAccountServices) + .insert( + DavAccountServicesCompanion.insert( + accountId: 'account', + providerProfileVersion: const Value(davProviderProfileVersion), + canonicalServiceUri: + 'https://cloud.example.test/cloud/remote.php/dav/', + canonicalOrigin: 'https://cloud.example.test', + principalHref: const Value( + 'https://cloud.example.test/cloud/remote.php/dav/principals/users/alex/', + ), + calendarHomeHref: const Value( + 'https://cloud.example.test/cloud/remote.php/dav/calendars/alex/', + ), + capabilitiesJson: const Value( + '{"hasPrincipal":true,"hasCalendarHome":true}', + ), + discoveredAtUtc: now, + lastValidatedAtUtc: const Value(now), + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: _collectionHref, + requestUri: 'https://cloud.example.test$_collectionHref', + displayName: 'Work', + supportedComponentMask: const Value(3), + supportedReportsJson: Value( + jsonEncode([ + '{DAV:}sync-collection', + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ]), + ), + currentUserPrivilegesJson: Value( + jsonEncode(['{DAV:}read', '{DAV:}write']), + ), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + lastInventoryAtUtc: const Value(now), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-collection', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: _collectionHref, + davCollectionId: const Value('collection'), + summary: 'Work', + createdAtLocal: _now.millisecondsSinceEpoch, + updatedAtLocal: _now.millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-collection', + davCollectionId: const Value('collection'), + title: 'Work', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} + +String _syncResponse({required String token, String? etag}) => + ''' + + ${etag == null ? '' : ''' + $_eventHref + $etag + HTTP/1.1 200 OK + '''} + $token +'''; + +String _multigetResponse(String body, String etag) => + ''' + + + $_eventHref + + $etag + + HTTP/1.1 200 OK + +'''; + +String _event(String summary) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:event@example.test\r +DTSTART:20260808T090000Z\r +DTEND:20260808T100000Z\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; diff --git a/test/dav/sync/dav_sync_engine_test.dart b/test/dav/sync/dav_sync_engine_test.dart new file mode 100644 index 0000000..7a4ffbb --- /dev/null +++ b/test/dav/sync/dav_sync_engine_test.dart @@ -0,0 +1,578 @@ +import 'dart:convert'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/http/dav_http_transport.dart'; +import 'package:busymax/src/dav/storage/dav_object_repository.dart'; +import 'package:busymax/src/dav/sync/dav_collection_remote_client.dart'; +import 'package:busymax/src/dav/sync/dav_sync_engine.dart'; +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:drift/drift.dart' show Value; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late AppDatabase database; + late DavObjectRepository repository; + var nextId = 0; + + setUp(() async { + database = AppDatabase(NativeDatabase.memory()); + repository = DavObjectRepository( + database: database, + idFactory: () => 'id-${nextId += 1}', + ); + }); + + tearDown(() => database.close()); + + test( + 'initial empty-token sync follows 507 pages and persists only final token', + () async { + await _seedCollection(database, syncCollection: true); + final remote = _FakeRemoteClient( + sync: (token) async => switch (token) { + '' => DavSyncPage( + changedMembers: [_member('a.ics', '"a1"')], + deletedHrefKeys: const {}, + nextSyncToken: 'intermediate-secret-token', + truncated: true, + ), + 'intermediate-secret-token' => DavSyncPage( + changedMembers: [_member('b.ics', '"b1"')], + deletedHrefKeys: const {}, + nextSyncToken: 'final-token', + truncated: false, + ), + _ => throw StateError('unexpected token'), + }, + fetch: (members) async => [ + for (final member in members) + _fetched(member, _event(member.hrefKey, member.hrefKey)), + ], + ); + var notificationCalls = 0; + final engine = _engine( + database, + repository, + remote, + onNotifications: (_) async => notificationCalls += 1, + ); + + final result = await engine.synchronize(correlationId: 'initial'); + + expect(remote.requestedTokens, ['', 'intermediate-secret-token']); + expect(result.pages, 2); + expect(result.initialOrRebaseline, isTrue); + expect(result.finalCursorValue, 'final-token'); + expect(await database.select(database.davObjects).get(), hasLength(2)); + final cursor = await database.select(database.syncCursors).getSingle(); + expect(cursor.cursorValue, 'final-token'); + expect(cursor.inProgressCursor, isNull); + expect(cursor.inProgressGeneration, isNull); + expect(notificationCalls, 1); + }, + ); + + test( + 'incremental sync applies add, change, and deletion atomically', + () async { + await _seedCollection(database, syncCollection: true); + await _runInitial(database, repository, { + 'a.ics': ('"a1"', _event('a', 'A')), + 'b.ics': ('"b1"', _event('b', 'B')), + }, token: 'baseline-token'); + final remote = _FakeRemoteClient( + sync: (token) async { + expect(token, 'baseline-token'); + return DavSyncPage( + changedMembers: [ + _member('a.ics', '"a2"'), + _member('c.ics', '"c1"'), + ], + deletedHrefKeys: {_href('b.ics')}, + nextSyncToken: 'next-token', + truncated: false, + ); + }, + fetch: (members) async => [ + for (final member in members) + _fetched( + member, + member.hrefKey.endsWith('a.ics') + ? _event('a', 'A changed') + : _event('c', 'C'), + ), + ], + ); + + final result = await _engine( + database, + repository, + remote, + ).synchronize(correlationId: 'incremental'); + + expect(result.initialOrRebaseline, isFalse); + final objects = await database.select(database.davObjects).get(); + expect(objects, hasLength(3)); + expect( + objects + .singleWhere((object) => object.hrefKey == _href('b.ics')) + .serverDeleted, + isTrue, + ); + expect( + objects + .singleWhere((object) => object.hrefKey == _href('a.ics')) + .rawIcsBody, + contains('A changed'), + ); + expect( + (await database.select(database.calendarEvents).get()) + .map((event) => event.title) + .toSet(), + {'A changed', 'C'}, + ); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'next-token', + ); + }, + ); + + test( + 'failure in a later multiget batch keeps baseline and cursor intact', + () async { + await _seedCollection(database, syncCollection: true); + await _runInitial(database, repository, { + 'old.ics': ('"old"', _event('old', 'Old')), + }, token: 'old-token'); + var fetchCalls = 0; + final remote = _FakeRemoteClient( + sync: (_) async => DavSyncPage( + changedMembers: [ + _member('new-a.ics', '"a"'), + _member('new-b.ics', '"b"'), + ], + deletedHrefKeys: const {}, + nextSyncToken: 'must-not-commit', + truncated: false, + ), + fetch: (members) async { + fetchCalls += 1; + if (fetchCalls == 2) { + throw const DavException( + kind: DavErrorKind.server, + code: 'InjectedLaterBatchFailure', + safeMessage: 'Injected failure.', + ); + } + return [_fetched(members.single, _event('new-a', 'New A'))]; + }, + ); + final engine = _engine( + database, + repository, + remote, + limits: const DavSyncLimits(maximumMembersPerMultiget: 1), + ); + + await expectLater( + engine.synchronize(correlationId: 'batch-failure'), + throwsA(isA()), + ); + + expect(fetchCalls, 2); + expect( + (await database.select(database.davObjects).get()).map( + (object) => object.hrefKey, + ), + [_href('old.ics')], + ); + final cursor = await database.select(database.syncCursors).getSingle(); + expect(cursor.cursorValue, 'old-token'); + expect(cursor.lastFailureCode, 'InjectedLaterBatchFailure'); + expect(cursor.inProgressGeneration, isNull); + }, + ); + + test( + 'changed member that disappears during multiget becomes a deletion', + () async { + await _seedCollection(database, syncCollection: true); + await _runInitial(database, repository, { + 'a.ics': ('"a1"', _event('a', 'A')), + }, token: 'old-token'); + final remote = _FakeRemoteClient( + sync: (_) async => DavSyncPage( + changedMembers: [_member('a.ics', '"a2"')], + deletedHrefKeys: const {}, + nextSyncToken: 'new-token', + truncated: false, + ), + fetch: (members) async => [ + DavFetchedMember.missing( + hrefKey: members.single.hrefKey, + requestUri: members.single.requestUri, + ), + ], + ); + + await _engine( + database, + repository, + remote, + ).synchronize(correlationId: 'race'); + + expect( + (await database.select(database.davObjects).getSingle()).serverDeleted, + isTrue, + ); + expect(await database.select(database.calendarEvents).get(), isEmpty); + expect( + (await database.select(database.syncCursors).getSingle()).cursorValue, + 'new-token', + ); + }, + ); + + test( + 'invalid token safely rebaselines without erasing the prior baseline first', + () async { + await _seedCollection(database, syncCollection: true); + await _runInitial(database, repository, { + 'old.ics': ('"old"', _event('old', 'Old')), + }, token: 'expired-token'); + final remote = _FakeRemoteClient( + sync: (token) async { + if (token == 'expired-token') { + throw const DavException( + kind: DavErrorKind.invalidSyncToken, + code: 'DavSyncTokenInvalid', + safeMessage: 'Expired.', + ); + } + expect(token, ''); + expect( + (await database.select(database.davObjects).getSingle()).rawIcsBody, + contains('Old'), + ); + expect( + (await database.select(database.syncCursors).getSingle()) + .cursorValue, + 'expired-token', + ); + return DavSyncPage( + changedMembers: [_member('replacement.ics', '"replacement"')], + deletedHrefKeys: const {}, + nextSyncToken: 'replacement-token', + truncated: false, + ); + }, + fetch: (members) async => [ + _fetched(members.single, _event('replacement', 'Replacement')), + ], + ); + + final result = await _engine( + database, + repository, + remote, + ).synchronize(correlationId: 'rebaseline'); + + expect(remote.requestedTokens, ['expired-token', '']); + expect(result.initialOrRebaseline, isTrue); + final objects = await database.select(database.davObjects).get(); + expect( + objects + .singleWhere((object) => object.hrefKey == _href('old.ics')) + .serverDeleted, + isTrue, + ); + expect( + objects + .singleWhere((object) => object.hrefKey == _href('replacement.ics')) + .serverDeleted, + isFalse, + ); + }, + ); + + test( + 'fallback inventory performs a complete ETag diff and snapshot cursor', + () async { + await _seedCollection(database, syncCollection: false); + var inventory = { + 'a.ics': ('"a1"', _event('a', 'A')), + 'b.ics': ('"b1"', _event('b', 'B')), + }; + final fetchedNames = []; + final remote = _FakeRemoteClient( + inventory: () async => DavMemberInventory( + members: [ + for (final entry in inventory.entries) + _member(entry.key, entry.value.$1), + ], + ), + fetch: (members) async => [ + for (final member in members) + _fetched( + member, + inventory[_name(member.hrefKey)]!.$2, + onFetched: fetchedNames.add, + ), + ], + ); + final engine = _engine(database, repository, remote); + + final first = await engine.synchronize(correlationId: 'fallback-1'); + expect(first.finalCursorKind, 'snapshot_generation'); + expect(fetchedNames.toSet(), {'a.ics', 'b.ics'}); + fetchedNames.clear(); + inventory = { + 'a.ics': ('"a1"', _event('a', 'A')), + 'c.ics': ('"c1"', _event('c', 'C')), + }; + + await engine.synchronize(correlationId: 'fallback-2'); + + expect(fetchedNames, ['c.ics']); + final objects = await database.select(database.davObjects).get(); + expect( + objects + .singleWhere((object) => object.hrefKey == _href('b.ics')) + .serverDeleted, + isTrue, + ); + expect( + (await database.select(database.syncCursors).getSingle()).cursorKind, + 'snapshot_generation', + ); + }, + ); + + test( + 'malformed fetched resource aborts promotion and records safe failure', + () async { + await _seedCollection(database, syncCollection: true); + final remote = _FakeRemoteClient( + sync: (_) async => DavSyncPage( + changedMembers: [_member('bad.ics', '"bad"')], + deletedHrefKeys: const {}, + nextSyncToken: 'bad-token', + truncated: false, + ), + fetch: (members) async => [ + _fetched(members.single, 'not iCalendar user content'), + ], + ); + + await expectLater( + _engine( + database, + repository, + remote, + ).synchronize(correlationId: 'malformed'), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.invalidCalendarData, + ), + ), + ); + + expect(await database.select(database.davObjects).get(), isEmpty); + final cursor = await database.select(database.syncCursors).getSingle(); + expect(cursor.cursorValue, '0'); + expect(cursor.lastFailureCode, isNot(contains('not iCalendar'))); + }, + ); +} + +final class _FakeRemoteClient implements DavCollectionRemoteClient { + _FakeRemoteClient({this.sync, this.inventory, required this.fetch}); + + final Future Function(String token)? sync; + final Future Function()? inventory; + final Future> Function(List) fetch; + final List requestedTokens = []; + + @override + Future> fetchMembers( + List members, { + required String correlationId, + required bool useCalendarMultiget, + DavCancellationToken? cancellationToken, + }) => fetch(members); + + @override + Future listMemberEtags({ + required String correlationId, + DavCancellationToken? cancellationToken, + }) => inventory!(); + + @override + Future syncCollectionPage({ + required String syncToken, + required String correlationId, + DavCancellationToken? cancellationToken, + }) { + requestedTokens.add(syncToken); + return sync!(syncToken); + } +} + +DavSyncEngine _engine( + AppDatabase database, + DavObjectRepository repository, + DavCollectionRemoteClient remote, { + DavSyncLimits limits = const DavSyncLimits(), + Future Function(String)? onNotifications, +}) => DavSyncEngine( + database: database, + objectRepository: repository, + remoteClient: remote, + accountId: 'account', + collectionId: 'collection', + provider: BusyProvider.nextcloud, + limits: limits, + nowUtc: () => DateTime.utc(2026, 8, 8, 12), + onNotificationsNeedRebuild: onNotifications, +); + +Future _runInitial( + AppDatabase database, + DavObjectRepository repository, + Map members, { + required String token, +}) async { + final remote = _FakeRemoteClient( + sync: (_) async => DavSyncPage( + changedMembers: [ + for (final entry in members.entries) _member(entry.key, entry.value.$1), + ], + deletedHrefKeys: const {}, + nextSyncToken: token, + truncated: false, + ), + fetch: (requested) async => [ + for (final member in requested) + _fetched(member, members[_name(member.hrefKey)]!.$2), + ], + ); + await _engine( + database, + repository, + remote, + ).synchronize(correlationId: 'seed'); +} + +DavRemoteMember _member(String name, String etag) => DavRemoteMember( + hrefKey: _href(name), + requestUri: Uri.parse('https://cloud.example.test${_href(name)}'), + etag: etag, +); + +DavFetchedMember _fetched( + DavRemoteMember member, + String body, { + void Function(String)? onFetched, +}) { + onFetched?.call(_name(member.hrefKey)); + return DavFetchedMember.live( + hrefKey: member.hrefKey, + requestUri: member.requestUri, + etag: member.etag, + contentType: 'text/calendar; charset=utf-8', + rawIcsBody: body, + ); +} + +String _href(String name) => '/remote.php/dav/calendars/alex/work/$name'; + +String _name(String href) => href.split('/').last; + +String _event(String uid, String summary) => + '''BEGIN:VCALENDAR\r +VERSION:2.0\r +PRODID:-//BusyMax Test//EN\r +BEGIN:VEVENT\r +UID:$uid@example.test\r +DTSTART:20260808T090000Z\r +DTEND:20260808T100000Z\r +SUMMARY:$summary\r +END:VEVENT\r +END:VCALENDAR\r +'''; + +Future _seedCollection( + AppDatabase database, { + required bool syncCollection, +}) async { + const now = '2026-08-08T12:00:00.000Z'; + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'account', + provider: 'nextcloud', + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: const Value('signed_in'), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + final reports = [ + if (syncCollection) '{DAV:}sync-collection', + '{urn:ietf:params:xml:ns:caldav}calendar-multiget', + ]; + const collectionHref = '/remote.php/dav/calendars/alex/work/'; + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: collectionHref, + requestUri: 'https://cloud.example.test$collectionHref', + displayName: 'Work', + supportedReportsJson: Value(jsonEncode(reports)), + supportedComponentMask: const Value(3), + readOnly: const Value(false), + eventProjectionEnabled: const Value(true), + taskProjectionEnabled: const Value(true), + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + await database + .into(database.calendarSources) + .insert( + CalendarSourcesCompanion.insert( + id: 'dav-calendar-collection', + accountId: 'account', + provider: 'nextcloud', + providerCalendarId: collectionHref, + davCollectionId: const Value('collection'), + summary: 'Work', + createdAtLocal: DateTime.parse(now).millisecondsSinceEpoch, + updatedAtLocal: DateTime.parse(now).millisecondsSinceEpoch, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-task-list-collection', + davCollectionId: const Value('collection'), + title: 'Work', + rawJson: '{}', + createdLocalAtUtc: now, + updatedLocalAtUtc: now, + ), + ); +} diff --git a/test/dav/xml/dav_xml_test.dart b/test/dav/xml/dav_xml_test.dart new file mode 100644 index 0000000..946fff5 --- /dev/null +++ b/test/dav/xml/dav_xml_test.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/xml/dav_xml.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const parser = DavXmlParser(); + + test('matches namespaces, not prefixes, and retains unknown properties', () { + final result = parser.parseMultistatus( + _xml(''' + + + /dav/calendars/alex/work/ + + + Work + Calendar + opaque + + HTTP/1.1 200 OK + + + + HTTP/1.1 404 Not Found + + + opaque-token + + '''), + ); + + expect(result.syncToken, 'opaque-token'); + expect(result.responses, hasLength(1)); + final response = result.responses.single; + expect(response.propstats, hasLength(2)); + expect( + response.successfulProperty(davNamespace, 'displayname')?.text, + 'Work', + ); + final unknown = response.successfulProperty( + 'urn:vendor:test', + 'future-property', + ); + expect(unknown?.element.attributes.single.name.local, 'flag'); + expect(unknown?.element.attributes.single.value, 'yes'); + expect(response.propstats.last.statusCode, 404); + expect( + response.isMissing, + isFalse, + reason: 'A 404 for one optional property does not remove the resource.', + ); + }); + + test('resource status 404 is a deletion and conditions are retained', () { + final result = parser.parseMultistatus( + _xml(''' + + + /removed.ics + HTTP/1.1 404 Not Found + + + + '''), + ); + + expect(result.responses.single.isMissing, isTrue); + expect(result.hasCondition(davNamespace, 'valid-sync-token'), isTrue); + }); + + test('rejects DTDs, external entities, and malformed status lines', () { + expect( + () => parser.parseMultistatus( + _xml( + ']>' + '', + ), + ), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'DavXmlDtdForbidden', + ), + ), + ); + expect( + () => parser.parseMultistatus( + _xml(''' + + /a200 OK + + '''), + ), + throwsA( + isA().having( + (error) => error.kind, + 'kind', + DavErrorKind.malformedStatus, + ), + ), + ); + }); + + test('enforces byte, depth, element, text, and UTF-8 limits', () { + void expectCode(DavXmlParser limited, Uint8List source, String code) { + expect( + () => limited.parseMultistatus(source), + throwsA( + isA().having((error) => error.code, 'code', code), + ), + ); + } + + expectCode( + const DavXmlParser(limits: DavXmlLimits(maximumBytes: 4)), + _xml(''), + 'DavXmlResponseTooLarge', + ); + expectCode( + const DavXmlParser(limits: DavXmlLimits(maximumDepth: 2)), + _xml(''), + 'DavXmlDepthLimitExceeded', + ); + expectCode( + const DavXmlParser(limits: DavXmlLimits(maximumElements: 1)), + _xml(''), + 'DavXmlElementLimitExceeded', + ); + expectCode( + const DavXmlParser(limits: DavXmlLimits(maximumTextBytes: 3)), + _xml( + 'text', + ), + 'DavXmlTextLimitExceeded', + ); + expectCode(parser, Uint8List.fromList([0xC3, 0x28]), 'DavXmlInvalidUtf8'); + }); +} + +Uint8List _xml(String source) => Uint8List.fromList(utf8.encode(source)); diff --git a/test/db/app_database_test.dart b/test/db/app_database_test.dart index 78adb98..339bcf3 100644 --- a/test/db/app_database_test.dart +++ b/test/db/app_database_test.dart @@ -7,7 +7,8 @@ import 'package:sqlite3/sqlite3.dart' as sqlite3; import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; import 'package:busymax/src/db/app_database.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/db/migrations.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; void main() { late AppDatabase database; @@ -20,7 +21,7 @@ void main() { await database.close(); }); - test('opens schema version 5 and creates required indexes', () async { + test('opens schema version 8 and creates required indexes', () async { final version = await database .customSelect('PRAGMA user_version') .getSingle(); @@ -31,22 +32,41 @@ void main() { ) .get(); - expect(version.data['user_version'], 5); + final taskColumns = await database + .customSelect('PRAGMA table_info(tasks)') + .get(); + + expect(version.data['user_version'], 8); + expect( + taskColumns.map((row) => row.read('name')), + contains('microsoft_checklist_items_json'), + ); expect(indexes.map((row) => row.data['name']).toSet(), { 'idx_accounts_provider', - 'idx_accounts_provider_account', + 'idx_accounts_remote_identity', + 'idx_dav_collections_href', + 'idx_dav_collections_sync', + 'idx_dav_objects_href', + 'idx_dav_objects_projection', + 'idx_dav_components_logical_key', + 'idx_dav_components_uid', 'idx_task_lists_account_title', 'idx_task_lists_dirty', + 'idx_task_lists_dav_collection', 'idx_tasks_dirty', 'idx_tasks_list_order', 'idx_tasks_status_due', 'idx_tasks_updated', + 'idx_tasks_dav_component', 'idx_calendar_events_dirty', 'idx_calendar_events_provider_id', 'idx_calendar_events_range', + 'idx_calendar_events_dav_occurrence', 'idx_calendar_sources_provider_id', 'idx_calendar_sources_visible', - 'idx_calendar_sync_states_scope', + 'idx_calendar_sources_dav_collection', + 'idx_sync_cursors_scope', + 'idx_pending_ops_dav_replay', 'idx_notification_schedule_due', }); }); @@ -222,6 +242,150 @@ void main() { expect(task.bodyContentType, 'html'); }); + test( + 'production-like schema 5 fixture migrates both providers without loss', + () async { + await database.close(); + final fixture = await _openSchema5Fixture(); + database = fixture.database; + + final version = await database + .customSelect('PRAGMA user_version') + .getSingle(); + expect(version.read('user_version'), 8); + + final accounts = await database.select(database.accounts).get(); + expect(accounts, hasLength(2)); + expect( + accounts + .map( + (account) => ( + account.provider, + account.authority, + account.providerAccountId, + account.credentialKind, + ), + ) + .toSet(), + { + ('google', 'https://accounts.google.com', 'g-sub', 'oauth'), + ( + 'microsoft', + 'https://login.microsoftonline.com/tenant-a', + 'm-sub', + 'oauth', + ), + }, + ); + + final tasks = await database.select(database.tasks).get(); + expect(tasks, hasLength(3)); + expect(tasks.every((task) => task.taskLocation == null), isTrue); + expect(tasks.every((task) => task.taskUrl == null), isTrue); + expect(tasks.every((task) => task.taskClassification == null), isTrue); + expect(tasks.every((task) => task.taskPinned == null), isTrue); + expect(tasks.every((task) => task.taskAlarmsJson == null), isTrue); + expect( + tasks.singleWhere((task) => task.id == 'g-child').parent, + 'g-parent', + ); + expect( + tasks.singleWhere((task) => task.id == 'm-recurring').recurrenceJson, + contains('weekly'), + ); + expect(await database.select(database.taskLists).get(), hasLength(2)); + + final pendingOps = await database.select(database.pendingOps).get(); + expect(pendingOps.map((op) => op.operationType).toSet(), { + 'create', + 'update', + 'delete', + }); + expect( + pendingOps.singleWhere((op) => op.id == 'op-update').baselineRawJson, + contains('Recurring task'), + ); + + final events = await database.select(database.calendarEvents).get(); + expect(events, hasLength(2)); + expect( + events.singleWhere((event) => event.id == 'g-event').recurrenceJson, + contains('RRULE:FREQ=WEEKLY'), + ); + expect( + await database.select(database.calendarSources).get(), + hasLength(2), + ); + + final cursors = await database.select(database.syncCursors).get(); + expect(cursors, hasLength(2)); + expect( + cursors + .map((cursor) => (cursor.cursorKind, cursor.cursorValue)) + .toSet(), + { + ('google_sync_token', 'g-token'), + ('microsoft_delta_link', 'https://graph.example/delta-2'), + }, + ); + expect( + await database + .customSelect( + "SELECT name FROM sqlite_master WHERE name = 'calendar_sync_states'", + ) + .getSingleOrNull(), + equals(null), + ); + + expect(await database.select(database.davCollections).get(), isEmpty); + expect(await database.select(database.davObjects).get(), isEmpty); + expect( + await database.customSelect('PRAGMA foreign_key_check').get(), + isEmpty, + ); + final accountColumns = await database + .customSelect('PRAGMA table_info(accounts)') + .get(); + final providerColumn = accountColumns.singleWhere( + (row) => row.read('name') == 'provider', + ); + expect(providerColumn.data['dflt_value'], equals(null)); + expect(providerColumn.read('notnull'), 1); + + await database.close(); + database = AppDatabase(NativeDatabase.memory()); + await fixture.directory.delete(recursive: true); + }, + ); + + test('schema 5 migration rejects an unsupported provider value', () async { + await database.close(); + final fixture = await _openSchema5Fixture( + prepare: (raw) { + raw.execute( + "UPDATE accounts SET provider = 'unsupported' " + "WHERE id = 'google:g-sub'", + ); + }, + ); + database = fixture.database; + + await expectLater( + database.customSelect('PRAGMA user_version').getSingle(), + throwsA( + isA().having( + (error) => error.code, + 'code', + 'unsupported_provider_value', + ), + ), + ); + + await database.close(); + database = AppDatabase(NativeDatabase.memory()); + await fixture.directory.delete(recursive: true); + }); + test( 'migration to v2 preserves pending ops with null baselineRawJson', () async { @@ -285,7 +449,7 @@ void main() { .getSingle(); final op = await database.pendingOpsDao.getOp('op-1'); - expect(version.data['user_version'], 5); + expect(version.data['user_version'], 8); expect(op, isNot(equals(null))); expect(op!.baselineRawJson, equals(null)); @@ -302,6 +466,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), @@ -358,3 +526,22 @@ PendingOpsCompanion _pendingOp({ } const _now = '2026-06-04T00:00:00.000Z'; + +Future<({AppDatabase database, Directory directory})> _openSchema5Fixture({ + void Function(sqlite3.Database raw)? prepare, +}) async { + final directory = await Directory.systemTemp.createTemp( + 'busymax-schema5-fixture-', + ); + final file = File('${directory.path}/busymax.sqlite'); + final raw = sqlite3.sqlite3.open(file.path); + try { + raw.execute( + File('test/fixtures/schema_v5_production_like.sql').readAsStringSync(), + ); + prepare?.call(raw); + } finally { + raw.close(); + } + return (database: AppDatabase(NativeDatabase(file)), directory: directory); +} diff --git a/test/demo/demo_profile_test.dart b/test/demo/demo_profile_test.dart index 2064b6a..bbd2218 100644 --- a/test/demo/demo_profile_test.dart +++ b/test/demo/demo_profile_test.dart @@ -5,7 +5,7 @@ import 'package:busymax/src/demo/demo_seed.dart'; import 'package:busymax/src/features/auth/data/auth_repository.dart'; import 'package:busymax/src/features/feedback/data/feedback_submission.dart'; import 'package:busymax/src/features/sync/account_sync_operations.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -75,10 +75,7 @@ void main() { isA(), ); expect(container.read(applicationMicrosoftOAuthServiceProvider), isNull); - expect( - container.read(oAuthTokenStoreProvider), - isA(), - ); + expect(container.read(secretStoreProvider), isA()); expect( container.read( taskRemoteApiClientForAccountProvider(busyMaxDemoAccountId), diff --git a/test/features/accounts/data/accounts_repository_test.dart b/test/features/accounts/data/accounts_repository_test.dart new file mode 100644 index 0000000..a429e28 --- /dev/null +++ b/test/features/accounts/data/accounts_repository_test.dart @@ -0,0 +1,57 @@ +import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('selector label keeps identical emails distinct across providers', () { + const google = AccountEntity( + id: 'google-account', + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: 'google-user', + authState: accountAuthStateSignedIn, + displayName: 'Personal account', + email: 'user@example.test', + ); + const nextcloud = AccountEntity( + id: 'nextcloud-account', + provider: BusyProvider.nextcloud, + authority: 'https://cloud.example.test', + providerAccountId: 'nextcloud-user', + authState: accountAuthStateSignedIn, + displayName: 'Personal account', + email: 'user@example.test', + ); + + expect(google.selectorLabel, 'Google · user@example.test'); + expect(nextcloud.selectorLabel, 'Nextcloud · user@example.test'); + }); + + test('Nextcloud selector falls back to profile name and server', () { + const account = AccountEntity( + id: 'nextcloud-account', + provider: BusyProvider.nextcloud, + authority: 'https://cloud.example.test:8443', + providerAccountId: 'nextcloud-user', + authState: accountAuthStateSignedIn, + displayName: 'Personal account', + ); + + expect( + account.selectorLabel, + 'Nextcloud · Personal account · cloud.example.test:8443', + ); + }); + + test('selector falls back to the provider when identity is unavailable', () { + const account = AccountEntity( + id: 'google-account', + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: 'google-user', + authState: accountAuthStateSignedIn, + ); + + expect(account.selectorLabel, 'Google'); + }); +} diff --git a/test/features/auth/data/auth_repository_test.dart b/test/features/auth/data/auth_repository_test.dart index 7d70b15..468f961 100644 --- a/test/features/auth/data/auth_repository_test.dart +++ b/test/features/auth/data/auth_repository_test.dart @@ -5,15 +5,16 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; import 'package:busymax/src/features/auth/data/auth_repository.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_surface.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_service.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import 'package:busymax/src/microsoft_todo/oauth/microsoft_oauth_service.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; void main() { late AppDatabase database; @@ -34,6 +35,20 @@ void main() { await database.close(); }); + test('DAV authentication failures expose only their safe message', () { + const error = DavException( + kind: DavErrorKind.authentication, + code: 'NextcloudLoginFlowStartRejected', + safeMessage: 'Nextcloud could not start browser authorization.', + statusCode: 401, + ); + + expect( + authErrorMessage(error), + 'Nextcloud could not start browser authorization.', + ); + }); + test('successful sign-in upserts signed-in account row', () async { final state = await repository.signIn(); @@ -129,7 +144,7 @@ void main() { test( 'loadSession trusts signed-in account rows without reading tokens', () async { - await _insertAccount(database, 'account-1', TaskProvider.google); + await _insertAccount(database, 'account-1', BusyProvider.google); oAuth.activeId = 'account-1'; oAuth.nextTokenSet = _tokenSet(scopes: {googleTasksReadOnlyScope}); @@ -146,7 +161,7 @@ void main() { test( 'markReconnectRequired keeps account row visible but not syncable', () async { - await _insertAccount(database, 'google:g', TaskProvider.google); + await _insertAccount(database, 'google:g', BusyProvider.google); oAuth.activeId = 'google:g'; await repository.markReconnectRequired('google:g'); @@ -166,8 +181,8 @@ void main() { ); test('markReconnectRequired removes only target notifications', () async { - await _insertAccount(database, 'google-a', TaskProvider.google); - await _insertAccount(database, 'google-b', TaskProvider.google); + await _insertAccount(database, 'google-a', BusyProvider.google); + await _insertAccount(database, 'google-b', BusyProvider.google); await _insertNotification(database, 'google-a'); await _insertNotification(database, 'google-b'); @@ -191,7 +206,7 @@ void main() { nowUtc: () => DateTime.utc(2026, 6, 4), ); const opaqueAccountId = 'opaque-account-id'; - await _insertAccount(database, opaqueAccountId, TaskProvider.microsoft); + await _insertAccount(database, opaqueAccountId, BusyProvider.microsoft); await repository.markReconnectRequired(opaqueAccountId); @@ -208,8 +223,8 @@ void main() { microsoftOAuth: microsoftOAuth, nowUtc: () => DateTime.utc(2026, 6, 4), ); - await _insertAccount(database, 'google-a', TaskProvider.google); - await _insertAccount(database, 'microsoft:m', TaskProvider.microsoft); + await _insertAccount(database, 'google-a', BusyProvider.google); + await _insertAccount(database, 'microsoft:m', BusyProvider.microsoft); await _insertNotification(database, 'google-a'); await _insertNotification(database, 'microsoft:m'); @@ -240,8 +255,8 @@ void main() { microsoftOAuth: microsoftOAuth, nowUtc: () => DateTime.utc(2026, 6, 4), ); - await _insertAccount(database, 'google:g', TaskProvider.google); - await _insertAccount(database, 'microsoft:m', TaskProvider.microsoft); + await _insertAccount(database, 'google:g', BusyProvider.google); + await _insertAccount(database, 'microsoft:m', BusyProvider.microsoft); final result = await repository.removeAccount( accountId: 'microsoft:m', @@ -301,7 +316,7 @@ void main() { ); test('removeAccount cascades pending offline operations', () async { - await _insertAccount(database, 'google-a', TaskProvider.google); + await _insertAccount(database, 'google-a', BusyProvider.google); await database .into(database.pendingOps) .insert( @@ -441,14 +456,19 @@ OAuthTokenSet _tokenSet({Set? scopes}) { Future _insertAccount( AppDatabase database, String id, - TaskProvider provider, + BusyProvider provider, ) { return database .into(database.accounts) .insert( AccountsCompanion.insert( id: id, - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: id, + credentialKind: 'oauth', authState: const Value('signed_in'), createdAtUtc: '2026-06-04T00:00:00.000Z', updatedAtUtc: '2026-06-04T00:00:00.000Z', @@ -479,7 +499,7 @@ class _FakeMicrosoftOAuthService extends MicrosoftOAuthService { : super( config: _config, httpClient: MockClient((request) async => http.Response('', 200)), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), ); diff --git a/test/features/auth/presentation/auth_routing_test.dart b/test/features/auth/presentation/auth_routing_test.dart index d2bb663..29e71a4 100644 --- a/test/features/auth/presentation/auth_routing_test.dart +++ b/test/features/auth/presentation/auth_routing_test.dart @@ -19,13 +19,13 @@ import 'package:busymax/src/features/schedule/presentation/schedule_workspace.da import 'package:busymax/src/features/settings/presentation/settings_screen.dart'; import 'package:busymax/src/features/sync/sync_auth_error.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_surface.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_service.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import 'package:busymax/src/platform/linux_header_bar_service.dart'; import 'package:busymax/src/platform/native_dialog_service.dart'; import 'package:busymax/src/schedule/schedule_scope.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; const _nativeDialogChannel = MethodChannel(nativeDialogChannelName); @@ -53,9 +53,7 @@ void main() { expect(find.text('Connect accounts'), findsOneWidget); expect( - find.text( - 'Connect Google and Microsoft accounts to sync calendars and tasks.', - ), + find.text('Connect calendars and tasks from one of these providers.'), findsOneWidget, ); expect( @@ -64,16 +62,24 @@ void main() { ), findsOneWidget, ); - expect( - find.textContaining('Add all Google and Microsoft accounts'), - findsNothing, - ); expect(find.text('Add Google account'), findsOneWidget); expect(find.text('Add Microsoft account'), findsOneWidget); + expect(find.text('Add Apple iCloud Calendar account'), findsOneWidget); + expect(find.text('Add Nextcloud account'), findsOneWidget); expect( tester.getTopLeft(find.text('Add Google account')).dy, lessThan(tester.getTopLeft(find.text('Add Microsoft account')).dy), ); + expect( + tester.getTopLeft(find.text('Add Microsoft account')).dy, + lessThan( + tester.getTopLeft(find.text('Add Apple iCloud Calendar account')).dy, + ), + ); + expect( + tester.getTopLeft(find.text('Add Apple iCloud Calendar account')).dy, + lessThan(tester.getTopLeft(find.text('Add Nextcloud account')).dy), + ); expect(find.text('Google'), findsNothing); expect(find.text('Microsoft To Do'), findsNothing); expect(find.text('Accounts'), findsNothing); @@ -225,8 +231,8 @@ void main() { expect(find.text('Choose system settings'), findsNothing); expect(find.byType(ScheduleWorkspace), findsNothing); expect(find.text('Accounts'), findsOneWidget); - expect(find.text('Albert Busy'), findsOneWidget); - expect(find.text('albert@example.com'), findsOneWidget); + expect(find.text('Test User'), findsOneWidget); + expect(find.text('user@example.com'), findsOneWidget); await tester.tap(find.text('Continue')); await tester.pumpAndSettle(); @@ -375,7 +381,7 @@ void main() { await tester.tap(find.text('Add Google account')); await tester.pumpAndSettle(); - expect(find.text(secureTokenStorageUnavailableMessage), findsOneWidget); + expect(find.text(secretStorageUnavailableMessage), findsOneWidget); expect(find.textContaining('PlatformException'), findsNothing); expect(find.textContaining('raw keyring message'), findsNothing); await _disposeApp(tester); @@ -415,7 +421,7 @@ void main() { await _insertAccount( database, id: 'google:existing', - provider: TaskProvider.google, + provider: BusyProvider.google, ); await _pumpApp(tester, database: database, oAuth: oAuth); await tester.pumpAndSettle(); @@ -444,7 +450,7 @@ void main() { await _insertAccount( database, id: 'google:existing', - provider: TaskProvider.google, + provider: BusyProvider.google, ); oAuth.signInCompleter = Completer(); await _pumpApp(tester, database: database, oAuth: oAuth); @@ -490,7 +496,7 @@ void main() { await _insertAccount( database, id: 'microsoft:existing', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, ); final syncCalls = <({String accountId, bool initial})>[]; await _pumpApp( @@ -536,7 +542,7 @@ void main() { await _insertAccount( database, id: 'microsoft:existing', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, ); oAuth.signInError = const OAuthException( 'OAuthCallbackTimeout', @@ -581,12 +587,12 @@ void main() { await _insertAccount( database, id: 'microsoft:existing', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, ); await _insertAccount( database, id: 'google:reconnect', - provider: TaskProvider.google, + provider: BusyProvider.google, authState: accountAuthStateReauthRequired, ); oAuth.signInCompleter = Completer(); @@ -667,7 +673,7 @@ void _expectExistingSessionSignedIn( Future _insertAccount( AppDatabase database, { required String id, - required TaskProvider provider, + required BusyProvider provider, String authState = accountAuthStateSignedIn, }) { return database @@ -675,7 +681,12 @@ Future _insertAccount( .insert( AccountsCompanion.insert( id: id, - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: id, + credentialKind: 'oauth', authState: Value(authState), displayName: Value(provider.displayName), createdAtUtc: '2026-06-04T00:00:00.000Z', @@ -742,12 +753,12 @@ class _FakeOAuthGateway implements OAuthGateway { Future fetchUserInfo(OAuthTokenSet tokenSet) async { return const GoogleUserInfo( subject: 'google-user-1', - name: 'Albert Busy', - email: 'albert@example.com', + name: 'Test User', + email: 'user@example.com', rawJson: { 'sub': 'google-user-1', - 'name': 'Albert Busy', - 'email': 'albert@example.com', + 'name': 'Test User', + 'email': 'user@example.com', }, ); } diff --git a/test/features/calendar/data/calendar_repository_test.dart b/test/features/calendar/data/calendar_repository_test.dart index 41fa6f8..08ac6f0 100644 --- a/test/features/calendar/data/calendar_repository_test.dart +++ b/test/features/calendar/data/calendar_repository_test.dart @@ -2,7 +2,7 @@ import 'package:busymax/src/calendar_providers/calendar_sync_dto.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; import 'package:busymax/src/features/calendar/presentation/event_editor_draft.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -25,7 +25,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'google:g', - provider: const Value('google'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'g', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', @@ -42,7 +45,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Original', ), @@ -52,7 +55,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Updated by provider', selected: true, @@ -68,7 +71,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Hidden at provider', selected: false, @@ -79,6 +82,23 @@ void main() { expect(source.selected, isFalse); }); + test('source entity exposes the provider primary-calendar flag', () async { + await repository.upsertSource( + accountId: 'google:g', + source: const CalendarSourceDto( + provider: BusyProvider.google, + providerCalendarId: 'calendar-1', + summary: 'Primary calendar', + primaryCalendar: true, + ), + ); + + final sources = await repository.watchSourcesForAccounts(const [ + 'google:g', + ]).first; + expect(sources.single.primaryCalendar, isTrue); + }); + test('provider upsert cannot resurrect a locally deleted source', () async { await _upsertSource(repository); await repository.deleteLocalSource(_sourceId); @@ -86,7 +106,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Still returned by provider', hidden: false, @@ -118,7 +138,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Calendar', hidden: true, @@ -128,7 +148,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Calendar', hidden: false, @@ -165,7 +185,7 @@ void main() { await repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Shared calendar', readOnly: true, @@ -247,7 +267,7 @@ Future _upsertSource(CalendarRepository repository) { return repository.upsertSource( accountId: 'google:g', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar-1', summary: 'Calendar', ), diff --git a/test/features/calendar/presentation/event_editor_test.dart b/test/features/calendar/presentation/event_editor_test.dart index 819bace..bb3a81b 100644 --- a/test/features/calendar/presentation/event_editor_test.dart +++ b/test/features/calendar/presentation/event_editor_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:busymax/src/calendar_providers/calendar_mutation.dart'; +import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; import 'package:busymax/src/features/calendar/presentation/event_editor.dart'; import 'package:busymax/src/features/calendar/presentation/event_editor_draft.dart'; @@ -10,7 +11,7 @@ import 'package:busymax/src/app/busymax_yaru_theme.dart'; import 'package:busymax/src/microsoft_calendar/microsoft_calendar_mapper.dart'; import 'package:busymax/src/platform/native_dialog_service.dart'; import 'package:busymax/src/platform/native_menu_service.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -39,6 +40,35 @@ void main() { .setMockMethodCallHandler(_nativeMenuChannel, null); }); + testWidgets( + 'missing calendar source is surfaced without a provider fallback', + (tester) async { + EventEditorDraft? saved; + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.newEvent( + accountId: 'missing-account', + sourceId: 'missing-source', + providerCalendarId: 'missing-calendar', + start: DateTime.utc(2026, 6, 8), + end: DateTime.utc(2026, 6, 8, 1), + ), + sources: const [], + onCancel: () {}, + onSave: (draft) => saved = draft, + ), + ), + ), + ); + + expect(find.text('No calendars synced yet.'), findsOneWidget); + expect(_headerButton(tester, 'Save').onPressed, isNull); + expect(saved, isNull); + }, + ); + testWidgets('editor actions use natural-width themed controls', ( tester, ) async { @@ -590,7 +620,7 @@ void main() { sources: _sources, onCancel: () {}, onSave: (_) {}, - onDelete: (eventId) => deletedEventId = eventId, + onDelete: (eventId, _) => deletedEventId = eventId, ), ), ), @@ -624,7 +654,7 @@ void main() { sources: _sources, onCancel: () {}, onSave: (_) {}, - onDelete: (eventId) => deletedEventId = eventId, + onDelete: (eventId, _) => deletedEventId = eventId, ), ), ), @@ -780,7 +810,7 @@ void main() { } testWidgets( - 'new event converts Google recurrence when Microsoft calendar is selected', + 'new event converts Google recurrence when Microsoft account is selected', (tester) async { EventEditorDraft? saved; await tester.pumpWidget( @@ -804,7 +834,7 @@ void main() { _comboRow(tester, 'Repeat').onSelected('weekly'); await tester.pump(); - _comboRow(tester, 'Calendar').onSelected('microsoft-source'); + _comboRow(tester, 'Account').onSelected('microsoft-account'); await tester.pump(); await tester.tap(_headerButtonFinder('Save')); @@ -816,7 +846,7 @@ void main() { ); testWidgets( - 'new event converts Microsoft recurrence when Google calendar is selected', + 'new event converts Microsoft recurrence when Google account is selected', (tester) async { EventEditorDraft? saved; await tester.pumpWidget( @@ -840,7 +870,7 @@ void main() { _comboRow(tester, 'Repeat').onSelected('weekly'); await tester.pump(); - _comboRow(tester, 'Calendar').onSelected('source'); + _comboRow(tester, 'Account').onSelected('account'); await tester.pump(); await tester.tap(_headerButtonFinder('Save')); @@ -877,6 +907,153 @@ void main() { expect(find.text('Repeat'), findsNothing); }); + testWidgets('DAV recurring event requires an explicit supported scope', ( + tester, + ) async { + EventEditorDraft? saved; + String? deletedId; + RecurringEventMutationScope? deletedScope; + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.existing( + eventId: 'dav-occurrence', + providerRecurringEventId: 'dav-series', + accountId: 'nextcloud-account', + sourceId: 'nextcloud-source', + providerCalendarId: '/calendars/work/', + title: 'Weekly planning', + allDay: false, + start: DateTime.utc(2026, 6, 8, 9), + end: DateTime.utc(2026, 6, 8, 10), + ), + sources: _nextcloudSources, + onCancel: () {}, + onSave: (draft) => saved = draft, + onDelete: (eventId, scope) { + deletedId = eventId; + deletedScope = scope; + }, + ), + ), + ), + ); + + expect(find.text('Recurring event scope'), findsOneWidget); + expect(find.text('Entire series'), findsOneWidget); + expect(find.text('This occurrence'), findsOneWidget); + expect(find.text('This and future (not available)'), findsOneWidget); + expect(_headerButton(tester, 'Save').onPressed, isNull); + expect(_actionRow(tester, 'Delete Event').onTap, isNull); + expect( + _actionRow(tester, 'This and future (not available)').enabled, + isFalse, + ); + + await tester.ensureVisible(find.text('Entire series')); + await tester.tap(find.text('Entire series')); + await tester.pump(); + expect(_headerButton(tester, 'Save').onPressed, isNotNull); + expect(_actionRow(tester, 'Delete Event').onTap, isNotNull); + await tester.tap(_headerButtonFinder('Save')); + expect( + saved?.recurringMutationScope, + RecurringEventMutationScope.entireSeries, + ); + + await tester.ensureVisible(find.text('This occurrence')); + await tester.tap(find.text('This occurrence')); + await tester.pump(); + await tester.ensureVisible(find.text('Delete Event')); + await tester.tap(find.text('Delete Event')); + expect(deletedId, 'dav-occurrence'); + expect(deletedScope, RecurringEventMutationScope.singleOccurrence); + }); + + testWidgets( + 'DAV event keeps guests display-only and exposes categories and alarms', + (tester) async { + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.existing( + eventId: 'dav-event', + accountId: 'nextcloud-account', + sourceId: 'nextcloud-source', + providerCalendarId: '/calendars/work/', + title: 'Planning', + allDay: false, + start: DateTime.utc(2026, 6, 8, 9), + end: DateTime.utc(2026, 6, 8, 10), + attendees: const [ + EventAttendeeDraft( + email: 'guest@example.test', + displayName: 'Guest', + ), + ], + categories: const ['Work'], + reminders: const { + 'useDefault': false, + 'overrides': [ + {'method': 'popup', 'minutes': 10}, + {'method': 'popup', 'minutes': 30}, + ], + }, + showAs: 'transparent', + visibilityOrSensitivity: 'confidential', + ), + sources: _nextcloudSources, + onCancel: () {}, + onSave: (_) {}, + ), + ), + ), + ); + + expect(find.text('guest@example.test'), findsOneWidget); + expect(find.text('Guest'), findsOneWidget); + expect(find.text('Add guest email'), findsNothing); + expect(find.text('Add guest'), findsNothing); + expect(find.text('Work'), findsWidgets); + expect(find.byType(BusyMaxComboRow), findsNWidgets(2)); + expect( + _comboRow(tester, 'Availability / Show as').selected, + 'transparent', + ); + expect(_comboRow(tester, 'Visibility').selected, 'confidential'); + }, + ); + + testWidgets('new Nextcloud recurrence uses RFC RRULE values', (tester) async { + EventEditorDraft? saved; + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.newEvent( + accountId: 'nextcloud-account', + sourceId: 'nextcloud-source', + providerCalendarId: '/calendars/work/', + start: DateTime.utc(2026, 6, 8, 9), + end: DateTime.utc(2026, 6, 8, 10), + ).copyWith(title: 'Planning'), + sources: _nextcloudSources, + onCancel: () {}, + onSave: (draft) => saved = draft, + ), + ), + ), + ); + + _comboRow(tester, 'Repeat').onSelected('weekly'); + await tester.pump(); + await tester.tap(_headerButtonFinder('Save')); + + expect(saved?.recurrence, ['RRULE:FREQ=WEEKLY;INTERVAL=1']); + }); + testWidgets('event editor does not show metadata fields', (tester) async { await tester.pumpWidget( localizedTestApp( @@ -895,7 +1072,7 @@ void main() { sources: _sources, onCancel: () {}, onSave: (_) {}, - onDelete: (_) {}, + onDelete: (_, _) {}, ), ), ), @@ -925,7 +1102,7 @@ void main() { sources: _sources, onCancel: () {}, onSave: (_) {}, - onDelete: (_) {}, + onDelete: (_, _) {}, ), ), ), @@ -961,7 +1138,7 @@ void main() { sources: _multipleSources, onCancel: () {}, onSave: (_) {}, - onDelete: (_) {}, + onDelete: (_, _) {}, ), ), ), @@ -977,6 +1154,10 @@ void main() { expect(calendarRow.selected, 'source'); expect(calendarRow.values, ['source']); expect(calendarRow.enabled, isFalse); + final accountRow = _comboRow(tester, 'Account'); + expect(accountRow.selected, 'account'); + expect(accountRow.values, ['account']); + expect(accountRow.enabled, isFalse); }); testWidgets('new event can still select any visible calendar', ( @@ -1008,10 +1189,112 @@ void main() { ), ); - expect(calendarRow.values, ['source', 'destination-source']); + expect(calendarRow.values, ['destination-source', 'source']); expect(calendarRow.enabled, isTrue); }); + testWidgets( + 'new event selects an account before choosing among its calendars', + (tester) async { + EventEditorDraft? saved; + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.newEvent( + accountId: 'google-account', + sourceId: 'google-work', + providerCalendarId: 'google-work-calendar', + start: DateTime.utc(2026, 6, 8, 9), + end: DateTime.utc(2026, 6, 8, 10), + ).copyWith(title: 'Planning'), + sources: _sameNamedCrossAccountSources, + accounts: _eventAccounts, + onCancel: () {}, + onSave: (draft) => saved = draft, + ), + ), + ), + ); + + var accountRow = _comboRow(tester, 'Account'); + var calendarRow = _comboRow(tester, 'Calendar'); + expect(accountRow.values, ['google-account', 'microsoft-account']); + expect(accountRow.selected, 'google-account'); + expect( + accountRow.labelFor('google-account'), + 'Google · personal@example.test', + ); + expect( + accountRow.labelFor('microsoft-account'), + 'Microsoft · work@example.test', + ); + expect(calendarRow.values, ['google-work']); + expect(calendarRow.labelFor('google-work'), 'Work'); + + final accountSemantics = tester.widget( + find.descendant( + of: find.byType(BusyMaxComboRow), + matching: find.byWidgetPredicate( + (widget) => + widget is Semantics && widget.properties.label == 'Account', + ), + ), + ); + expect( + accountSemantics.properties.value, + 'Google · personal@example.test', + ); + + accountRow.onSelected('microsoft-account'); + await tester.pump(); + + accountRow = _comboRow(tester, 'Account'); + calendarRow = _comboRow(tester, 'Calendar'); + expect(accountRow.selected, 'microsoft-account'); + expect(calendarRow.values, ['microsoft-work']); + expect(calendarRow.selected, 'microsoft-work'); + expect(calendarRow.labelFor('microsoft-work'), 'Work'); + + await tester.tap(_headerButtonFinder('Save')); + expect(saved?.accountId, 'microsoft-account'); + expect(saved?.sourceId, 'microsoft-work'); + expect(saved?.providerCalendarId, 'microsoft-work-calendar'); + }, + ); + + testWidgets('changing event account selects its primary writable calendar', ( + tester, + ) async { + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: EventEditor( + initialDraft: EventEditorDraft.newEvent( + accountId: 'google-account', + sourceId: 'google-work', + providerCalendarId: 'google-work-calendar', + start: DateTime.utc(2026, 6, 8, 9), + end: DateTime.utc(2026, 6, 8, 10), + ), + sources: _sourcesWithMicrosoftPrimary, + accounts: _eventAccounts, + onCancel: () {}, + onSave: (_) {}, + ), + ), + ), + ); + + _comboRow(tester, 'Account').onSelected('microsoft-account'); + await tester.pump(); + + final calendarRow = _comboRow(tester, 'Calendar'); + expect(calendarRow.values, ['microsoft-primary', 'microsoft-work']); + expect(calendarRow.selected, 'microsoft-primary'); + expect(calendarRow.labelFor(calendarRow.selected), 'Calendar'); + }); + testWidgets('Google event editor saves multiple reminder overrides', ( tester, ) async { @@ -1500,13 +1783,13 @@ void main() { ).readAsStringSync(); final titleLabelIndex = editor.indexOf('labelText: l10n.title'); final locationLabelIndex = editor.indexOf('labelText: l10n.location'); - final calendarGroupIndex = editor.indexOf( - 'BusyMaxGroupedList(filled: true, children: [_calendarRow()])', + final destinationGroupIndex = editor.indexOf( + 'children: [_accountRow(), _calendarRow()]', ); expect(titleLabelIndex, isNonNegative); expect(locationLabelIndex, greaterThan(titleLabelIndex)); - expect(calendarGroupIndex, greaterThan(locationLabelIndex)); + expect(destinationGroupIndex, greaterThan(locationLabelIndex)); }); test('event dropdown fields do not render duplicate section labels', () { @@ -1717,6 +2000,17 @@ Finder _headerButtonFinder(String label) { .first; } +ElevatedButton _headerButton(WidgetTester tester, String label) => + tester.widget(_headerButtonFinder(label)); + +BusyMaxActionRow _actionRow(WidgetTester tester, String title) { + return tester.widget( + find.byWidgetPredicate( + (widget) => widget is BusyMaxActionRow && widget.title == title, + ), + ); +} + BusyMaxComboRow _comboRow(WidgetTester tester, String title) { return tester.widget>( find.byWidgetPredicate( @@ -1791,11 +2085,76 @@ void _focusEditorShortcuts(WidgetTester tester) { focusWidget.focusNode!.requestFocus(); } +const _eventAccounts = [ + AccountEntity( + id: 'google-account', + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: 'google-user', + authState: accountAuthStateSignedIn, + displayName: 'Personal account', + email: 'personal@example.test', + ), + AccountEntity( + id: 'microsoft-account', + provider: BusyProvider.microsoft, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: 'microsoft-user', + authState: accountAuthStateSignedIn, + displayName: 'Work account', + email: 'work@example.test', + ), +]; + +const _sameNamedCrossAccountSources = [ + CalendarSourceEntity( + id: 'google-work', + accountId: 'google-account', + provider: BusyProvider.google, + providerCalendarId: 'google-work-calendar', + summary: 'Work', + selected: true, + hidden: false, + readOnly: false, + isDeleted: false, + backgroundColor: '#3584e4', + ), + CalendarSourceEntity( + id: 'microsoft-work', + accountId: 'microsoft-account', + provider: BusyProvider.microsoft, + providerCalendarId: 'microsoft-work-calendar', + summary: 'Work', + selected: true, + hidden: false, + readOnly: false, + isDeleted: false, + backgroundColor: '#9141ac', + ), +]; + +const _sourcesWithMicrosoftPrimary = [ + ..._sameNamedCrossAccountSources, + CalendarSourceEntity( + id: 'microsoft-primary', + accountId: 'microsoft-account', + provider: BusyProvider.microsoft, + providerCalendarId: 'microsoft-primary-calendar', + summary: 'Calendar', + selected: true, + hidden: false, + readOnly: false, + isDeleted: false, + primaryCalendar: true, + backgroundColor: '#e01b24', + ), +]; + const _sources = [ CalendarSourceEntity( id: 'source', accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', selected: true, @@ -1811,7 +2170,7 @@ const _multipleSources = [ CalendarSourceEntity( id: 'destination-source', accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-2', summary: 'Personal', selected: true, @@ -1822,11 +2181,27 @@ const _multipleSources = [ ), ]; +const _nextcloudSources = [ + CalendarSourceEntity( + id: 'nextcloud-source', + accountId: 'nextcloud-account', + provider: BusyProvider.nextcloud, + providerCalendarId: '/calendars/work/', + davCollectionId: 'nextcloud-collection', + summary: 'Nextcloud Work', + selected: true, + hidden: false, + readOnly: false, + isDeleted: false, + backgroundColor: '#0082c9', + ), +]; + const _microsoftSources = [ CalendarSourceEntity( id: 'microsoft-source', accountId: 'microsoft-account', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'ms-cal-1', summary: 'Outlook', selected: true, diff --git a/test/features/notifications/notification_schedule_service_test.dart b/test/features/notifications/notification_schedule_service_test.dart index 656719a..a7ad169 100644 --- a/test/features/notifications/notification_schedule_service_test.dart +++ b/test/features/notifications/notification_schedule_service_test.dart @@ -5,7 +5,7 @@ import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; import 'package:busymax/src/features/calendar/presentation/event_editor_draft.dart'; import 'package:busymax/src/features/notifications/notification_schedule_service.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -23,12 +23,12 @@ void main() { await _insertAccount( database, id: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, ); await _insertAccount( database, id: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, ); }); @@ -40,7 +40,7 @@ void main() { await _upsertEvent( database, accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, remindersJson: { 'useDefault': false, 'overrides': [ @@ -65,7 +65,7 @@ void main() { await _upsertEvent( database, accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, remindersJson: {'useDefault': true}, sourceRawJson: { 'id': 'cal-1', @@ -92,7 +92,7 @@ void main() { await _upsertEvent( database, accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, remindersJson: {'useDefault': false, 'overrides': const []}, sourceRawJson: { 'id': 'cal-1', @@ -115,7 +115,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 30}, ); @@ -132,7 +132,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, startDateTime: '2026-06-08T09:00:00', startTimeZone: 'UTC', remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 30}, @@ -151,7 +151,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, startDateTime: '2026-06-08T09:00:00', startTimeZone: 'America/Vancouver', remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 30}, @@ -174,7 +174,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, startDateTime: '2026-06-08T04:26:00.000Z', remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 5}, ); @@ -192,7 +192,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 10}, ); await service.rebuildUpcomingEventNotifications('microsoft:m'); @@ -217,7 +217,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 10}, ); await service.rebuildUpcomingEventNotifications('microsoft:m'); @@ -245,7 +245,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 10}, ); await service.rebuildUpcomingEventNotifications('microsoft:m'); @@ -261,7 +261,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, startDateTime: '2026-06-08T09:45:00.000Z', remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 10}, ); @@ -282,7 +282,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 10}, ); await service.rebuildUpcomingEventNotifications('microsoft:m'); @@ -303,7 +303,7 @@ void main() { await _upsertEvent( database, accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, startDateTime: '2026-06-08T04:26:00.000Z', remindersJson: {'isReminderOn': true, 'reminderMinutesBeforeStart': 5}, ); @@ -323,7 +323,7 @@ void main() { await repository.upsertSource( accountId: 'microsoft:m', source: const CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'cal-1', summary: 'Calendar', ), @@ -358,7 +358,7 @@ void main() { await _upsertEvent( database, accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, remindersJson: { 'overrides': [ {'method': 'popup', 'minutes': 10}, @@ -456,14 +456,19 @@ void main() { Future _insertAccount( AppDatabase database, { required String id, - required TaskProvider provider, + required BusyProvider provider, }) { return database .into(database.accounts) .insert( AccountsCompanion.insert( id: id, - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: id, + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', @@ -475,7 +480,7 @@ Future _insertAccount( Future _upsertEvent( AppDatabase database, { required String accountId, - required TaskProvider provider, + required BusyProvider provider, required Object remindersJson, String startDateTime = '2026-06-08T09:00:00.000Z', String? startTimeZone, @@ -515,7 +520,7 @@ Future _expectSourceUpdateRemovesEventReminder({ await _upsertEvent( database, accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, remindersJson: { 'overrides': [ {'method': 'popup', 'minutes': 10}, diff --git a/test/features/notifications/notification_scheduler_test.dart b/test/features/notifications/notification_scheduler_test.dart index 0c43645..14d57d1 100644 --- a/test/features/notifications/notification_scheduler_test.dart +++ b/test/features/notifications/notification_scheduler_test.dart @@ -2,7 +2,7 @@ import 'package:busymax/src/app/app_settings.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/notifications/desktop_notification_service.dart'; import 'package:busymax/src/features/notifications/notification_scheduler.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; @@ -33,7 +33,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'microsoft:m', - provider: Value(TaskProvider.microsoft.storageValue), + provider: BusyProvider.microsoft.storageValue, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: 'm', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', @@ -153,7 +156,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'google:g', - provider: Value(TaskProvider.google.storageValue), + provider: BusyProvider.google.storageValue, + authority: 'https://accounts.google.com', + providerAccountId: 'g', + credentialKind: 'oauth', authState: const Value('signed_out'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', diff --git a/test/features/schedule/presentation/schedule_sidebar_provider_test.dart b/test/features/schedule/presentation/schedule_sidebar_provider_test.dart new file mode 100644 index 0000000..9105d6b --- /dev/null +++ b/test/features/schedule/presentation/schedule_sidebar_provider_test.dart @@ -0,0 +1,146 @@ +import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; +import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; +import 'package:busymax/src/features/schedule/presentation/schedule_sidebar.dart'; +import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../test_localized_app.dart'; + +void main() { + testWidgets('sidebar task-list labels map every provider explicitly', ( + tester, + ) async { + late List labels; + await tester.pumpWidget( + localizedTestApp( + child: Builder( + builder: (context) { + labels = [ + for (final entry in <(BusyProvider, String)>[ + (BusyProvider.google, 'Google list'), + (BusyProvider.microsoft, 'Microsoft list'), + (BusyProvider.appleICloud, 'Apple list'), + (BusyProvider.nextcloud, 'Project Tasks'), + ]) + scheduleTaskListLabel( + context, + _account(entry.$1), + _taskList(_account(entry.$1).id, entry.$2), + ), + ]; + return const SizedBox.shrink(); + }, + ), + ), + ); + + expect(labels, [ + 'Google Tasks · Google list', + 'Microsoft To Do · Microsoft list', + 'Apple iCloud · Apple list', + 'Nextcloud Tasks · Project Tasks', + ]); + expect(labels.last, isNot(contains('Microsoft To Do'))); + }); + + test('provider links never fall through to a different provider', () { + final google = _account(BusyProvider.google); + final microsoft = _account(BusyProvider.microsoft); + final apple = _account(BusyProvider.appleICloud); + final nextcloud = _account(BusyProvider.nextcloud); + + expect( + scheduleCalendarProviderWebUri( + google, + _calendarSource(google, providerCalendarId: 'calendar-id'), + ), + Uri.parse('https://calendar.google.com/calendar/u/0/r?cid=calendar-id'), + ); + expect( + scheduleTaskProviderWebUri(google), + Uri.parse('https://tasks.google.com/'), + ); + expect( + scheduleCalendarProviderWebUri(microsoft, _calendarSource(microsoft)), + Uri.parse('https://outlook.live.com/calendar/0/view/month'), + ); + expect( + scheduleTaskProviderWebUri(microsoft), + Uri.parse('https://to-do.office.com/tasks/'), + ); + expect( + scheduleCalendarProviderWebUri(apple, _calendarSource(apple)), + isNull, + ); + expect(scheduleTaskProviderWebUri(apple), isNull); + expect( + scheduleCalendarProviderWebUri(nextcloud, _calendarSource(nextcloud)), + Uri.parse('https://cloud.example.test/nextcloud'), + ); + expect( + scheduleTaskProviderWebUri(nextcloud), + Uri.parse('https://cloud.example.test/nextcloud'), + ); + }); + + test('Nextcloud provider links reject mismatched or unsafe account data', () { + final nextcloud = _account(BusyProvider.nextcloud); + final mismatchedSource = _calendarSource(_account(BusyProvider.google)); + const unsafeAccount = AccountEntity( + id: 'nextcloud:unsafe', + provider: BusyProvider.nextcloud, + authority: 'https://user@cloud.example.test', + providerAccountId: 'unsafe', + authState: accountAuthStateSignedIn, + ); + + expect(scheduleCalendarProviderWebUri(nextcloud, mismatchedSource), isNull); + expect(scheduleTaskProviderWebUri(unsafeAccount), isNull); + }); +} + +AccountEntity _account(BusyProvider provider) { + final id = '${provider.storageValue}:account'; + return AccountEntity( + id: id, + provider: provider, + authority: switch (provider) { + BusyProvider.google => 'https://accounts.google.com', + BusyProvider.microsoft => 'https://login.microsoftonline.com/common', + BusyProvider.appleICloud => 'https://caldav.icloud.com', + BusyProvider.nextcloud => 'https://cloud.example.test/nextcloud', + }, + providerAccountId: 'account', + authState: accountAuthStateSignedIn, + ); +} + +TaskListEntity _taskList(String accountId, String title) { + return TaskListEntity( + accountId: accountId, + id: 'list', + title: title, + localDirty: false, + pendingDelete: false, + rawJson: '{}', + ); +} + +CalendarSourceEntity _calendarSource( + AccountEntity account, { + String providerCalendarId = 'calendar', +}) { + return CalendarSourceEntity( + id: 'source', + accountId: account.id, + provider: account.provider, + providerCalendarId: providerCalendarId, + summary: 'Calendar', + selected: true, + hidden: false, + readOnly: false, + isDeleted: false, + ); +} diff --git a/test/features/schedule/presentation/schedule_views_test.dart b/test/features/schedule/presentation/schedule_views_test.dart index bd69cc5..9d11f75 100644 --- a/test/features/schedule/presentation/schedule_views_test.dart +++ b/test/features/schedule/presentation/schedule_views_test.dart @@ -14,10 +14,11 @@ import 'package:busymax/src/features/schedule/presentation/schedule_item_exporte import 'package:busymax/src/features/schedule/presentation/mini_calendar.dart'; import 'package:busymax/src/features/schedule/presentation/schedule_month_view.dart'; import 'package:busymax/src/features/schedule/presentation/schedule_year_view.dart'; +import 'package:busymax/src/features/tasks/domain/task_checklist_item.dart'; import 'package:busymax/src/platform/gtk_font_service.dart'; import 'package:busymax/src/schedule/schedule_item.dart'; import 'package:busymax/src/schedule/schedule_range.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1567,7 +1568,7 @@ void main() { final event = CalendarScheduleItem( id: 'event:read-only', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:shared', providerCalendarId: 'shared', title: 'Shared calendar event', @@ -1613,7 +1614,7 @@ void main() { final event = CalendarScheduleItem( id: 'event:long', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'A detailed event with a deliberately long title for sizing', @@ -1717,7 +1718,7 @@ void main() { final task = TaskScheduleItem( id: 'task:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Submit report', completed: false, @@ -1760,7 +1761,7 @@ void main() { final event = CalendarScheduleItem( id: 'event:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'calendar:primary', providerCalendarId: 'cal-1', title: 'Design review', @@ -1806,7 +1807,7 @@ void main() { final event = CalendarScheduleItem( id: 'event:localized-reminders', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Termin', @@ -1850,7 +1851,7 @@ void main() { final task = TaskScheduleItem( id: 'task:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Submit report', completed: false, @@ -2048,7 +2049,7 @@ void main() { final event = CalendarScheduleItem( id: 'event:1', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Review, plan', @@ -2061,7 +2062,7 @@ void main() { final task = TaskScheduleItem( id: 'task:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Submit report', completed: true, @@ -2118,7 +2119,7 @@ void main() { CalendarScheduleItem( id: 'event:past', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Yesterday event', @@ -2140,7 +2141,7 @@ void main() { TaskScheduleItem( id: 'task:overdue', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Pay invoice', completed: false, @@ -2151,7 +2152,7 @@ void main() { TaskScheduleItem( id: 'task:completed-overdue', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Completed old task', completed: true, @@ -2163,7 +2164,7 @@ void main() { const TaskScheduleItem( id: 'task:no-date', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'tasks:inbox', title: 'Plan someday', completed: false, @@ -2198,6 +2199,306 @@ void main() { ); }); + testWidgets('agenda nests task children and Microsoft checklist steps', ( + tester, + ) async { + final selectedDate = DateTime(2026, 1, 15); + bool? checklistCompleted; + String? selectedTaskId; + const googleParent = TaskScheduleItem( + id: 'parent', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Parent task', + completed: false, + allDay: true, + hasSubtasks: true, + sourceName: 'Inbox', + ); + const googleChild = TaskScheduleItem( + id: 'child', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Child task', + completed: false, + allDay: true, + parentId: 'parent', + parentTitle: 'Parent task', + hierarchyDepth: 1, + hasSubtasks: true, + sourceName: 'Inbox', + ); + const googleGrandchild = TaskScheduleItem( + id: 'grandchild', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Grandchild task', + completed: false, + allDay: true, + parentId: 'child', + parentTitle: 'Child task', + hierarchyDepth: 2, + sourceName: 'Inbox', + ); + const checklistItem = TaskChecklistItemEntity( + id: 'step-1', + title: 'Checklist step', + completed: false, + rawJson: { + 'id': 'step-1', + 'displayName': 'Checklist step', + 'isChecked': false, + }, + ); + const microsoftParent = TaskScheduleItem( + id: 'ms-parent', + accountId: 'microsoft:m', + provider: BusyProvider.microsoft, + sourceId: 'microsoft-list', + title: 'Microsoft parent', + completed: false, + allDay: true, + hasSubtasks: true, + checklistItems: [checklistItem], + sourceName: 'Tasks', + ); + + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: SizedBox( + width: 800, + height: 700, + child: ScheduleAgendaView( + range: ScheduleRange.week(selectedDate), + items: const [ + googleGrandchild, + googleChild, + microsoftParent, + googleParent, + ], + onItemSelected: (_, item, [_]) { + if (item is TaskScheduleItem) selectedTaskId = item.id; + }, + onTaskCompletionChanged: (_, _) {}, + onChecklistItemCompletionChanged: (_, _, completed) { + checklistCompleted = completed; + }, + ), + ), + ), + ), + ); + await tester.pump(); + + final googleGroup = find.byKey( + const ValueKey('agenda-task-group-google:g-google-list-parent'), + ); + final googleChildRow = find.byKey( + const ValueKey('agenda-subtask-google:g-google-list-child'), + ); + final googleGrandchildRow = find.byKey( + const ValueKey('agenda-subtask-google:g-google-list-grandchild'), + ); + final microsoftGroup = find.byKey( + const ValueKey('agenda-task-group-microsoft:m-microsoft-list-ms-parent'), + ); + final checklistRow = find.byKey( + const ValueKey('agenda-checklist-ms-parent-step-1'), + ); + + expect(googleGroup, findsOneWidget); + expect( + find.descendant(of: googleGroup, matching: googleChildRow), + findsOneWidget, + ); + expect( + find.descendant(of: googleGroup, matching: googleGrandchildRow), + findsOneWidget, + ); + expect( + find.descendant(of: microsoftGroup, matching: checklistRow), + findsOneWidget, + ); + expect( + tester.getTopLeft(find.text('Parent task')).dy, + lessThan(tester.getTopLeft(find.text('Child task')).dy), + ); + expect( + tester.getTopLeft(find.text('Child task')).dx, + greaterThan(tester.getTopLeft(find.text('Parent task')).dx), + ); + expect( + tester.getTopLeft(find.text('Grandchild task')).dx, + greaterThan(tester.getTopLeft(find.text('Child task')).dx), + ); + expect(find.textContaining('Parent: Parent task'), findsNothing); + expect(find.text('Checklist step'), findsOneWidget); + + await tester.tap(find.text('Child task')); + await tester.pump(); + expect(selectedTaskId, 'child'); + + await tester.tap( + find.descendant(of: checklistRow, matching: find.byType(YaruCheckbox)), + ); + await tester.pump(); + expect(checklistCompleted, isTrue); + }); + + testWidgets('agenda groups detached siblings under one parent context', ( + tester, + ) async { + final selectedDate = DateTime(2026, 1, 15); + const firstChild = TaskScheduleItem( + id: 'child-1', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'First child', + completed: false, + allDay: true, + parentId: 'parent', + parentTitle: 'Parent outside this section', + hierarchyDepth: 1, + sourceName: 'Inbox', + ); + const secondChild = TaskScheduleItem( + id: 'child-2', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Second child', + completed: false, + allDay: true, + parentId: 'parent', + parentTitle: 'Parent outside this section', + hierarchyDepth: 1, + sourceName: 'Inbox', + ); + + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: SizedBox( + width: 800, + height: 700, + child: ScheduleAgendaView( + range: ScheduleRange.week(selectedDate), + items: const [firstChild, secondChild], + onItemSelected: (_, _, [_]) {}, + onTaskCompletionChanged: (_, _) {}, + ), + ), + ), + ), + ); + await tester.pump(); + + final detachedGroup = find.byKey( + const ValueKey('agenda-detached-task-group-google:g-google-list-parent'), + ); + expect(detachedGroup, findsOneWidget); + expect( + find.descendant(of: detachedGroup, matching: find.text('First child')), + findsOneWidget, + ); + expect( + find.descendant(of: detachedGroup, matching: find.text('Second child')), + findsOneWidget, + ); + expect( + find.textContaining('Parent: Parent outside this section'), + findsOneWidget, + ); + }); + + testWidgets('agenda keeps an undated Google child under its overdue parent', ( + tester, + ) async { + final selectedDate = DateTime(2026, 1, 15); + final parent = TaskScheduleItem( + id: 'dated-parent', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Dated parent', + completed: false, + allDay: true, + start: selectedDate.subtract(const Duration(days: 1)), + hasSubtasks: true, + sourceName: 'Projects', + ); + const child = TaskScheduleItem( + id: 'undated-child', + accountId: 'google:g', + provider: BusyProvider.google, + sourceId: 'google-list', + title: 'Undated child', + completed: false, + allDay: true, + parentId: 'dated-parent', + parentTitle: 'Dated parent', + hierarchyDepth: 1, + sourceName: 'Projects', + ); + + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: SizedBox( + width: 800, + height: 700, + child: ScheduleAgendaView( + range: ScheduleRange( + start: selectedDate, + end: selectedDate.add(const Duration(days: 7)), + ), + items: [child, parent], + onItemSelected: (_, _, [_]) {}, + onTaskCompletionChanged: (_, _) {}, + ), + ), + ), + ), + ); + await tester.pump(); + + final parentGroup = find.byKey( + const ValueKey('agenda-task-group-google:g-google-list-dated-parent'), + ); + final childRow = find.byKey( + const ValueKey('agenda-subtask-google:g-google-list-undated-child'), + ); + expect(parentGroup, findsOneWidget); + expect( + find.descendant(of: parentGroup, matching: childRow), + findsOneWidget, + ); + expect( + find.byKey( + const ValueKey('agenda-task-group-google:g-google-list-undated-child'), + ), + findsNothing, + ); + expect( + find.byKey( + const ValueKey( + 'agenda-detached-task-group-google:g-google-list-dated-parent', + ), + ), + findsNothing, + ); + expect(find.text('No date'), findsOneWidget); + expect( + tester.getTopLeft(find.text('Dated parent')).dy, + lessThan(tester.getTopLeft(find.text('Undated child')).dy), + ); + }); + testWidgets('agenda view stays blank instead of showing empty-state card', ( tester, ) async { @@ -2278,7 +2579,7 @@ void main() { TaskScheduleItem( id: 'task:$index', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'tasks:inbox', title: 'Task $index', completed: false, @@ -2324,7 +2625,7 @@ void main() { TaskScheduleItem( id: 'task:overdue', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'tasks:inbox', title: 'Pay invoice', completed: false, @@ -2335,7 +2636,7 @@ void main() { const TaskScheduleItem( id: 'task:no-date', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'tasks:inbox', title: 'Plan someday', completed: false, @@ -2484,7 +2785,10 @@ void main() { expect(source, contains('calendarRepositoryProvider).updateLocalEvent')); expect(source, contains('_requestCalendarMutationSync(draft.accountId)')); - expect(source, contains('.deleteLocalEvent(eventId)')); + expect( + source, + contains('.deleteLocalEvent(eventId, recurringScope: recurringScope)'), + ); expect(source, contains('_requestCalendarMutationSync(accountId)')); expect(source, contains('accountSyncOperationsProvider')); expect(source, isNot(contains('signedInSyncRunnerProvider)(accountId'))); @@ -2499,7 +2803,8 @@ void main() { expect(source, contains('ScheduleItemDetailsAction.export')); expect(source, contains('ScheduleItemDetailsAction.edit')); expect(source, contains('ScheduleItemDetailsAction.delete')); - expect(source, contains('exportScheduleItemWithSaveDialog(item)')); + expect(source, contains('exportScheduleItemWithSaveDialog(')); + expect(source, contains('rawICalendar: rawICalendar')); expect(source, isNot(contains('exportScheduleItemToDownloads(item)'))); expect(source, contains('void _editItem(')); expect(source, contains('Future _deleteItem(')); @@ -3685,6 +3990,7 @@ void main() { expect(source, contains('final noDateTasks = repository.listNoDateTasks')); expect(source, contains('limit: _agendaNoDateTaskLimit')); expect(source, contains('showCompletedTasks: false')); + expect(source, contains('repository.includeTaskAncestors')); expect(source, contains('hasMoreOverdueTasks: overduePage.hasMore')); expect(source, contains('hasMoreNoDateTasks: noDatePage.hasMore')); expect(source, contains('List _agendaItems')); @@ -3697,12 +4003,14 @@ void main() { expect(source, contains('return !item.completed;')); }); - test('agenda task markers use task list icons, not checkbox icons', () { + test('agenda task markers distinguish hierarchy without checkbox icons', () { final agenda = File( 'lib/src/features/schedule/presentation/schedule_agenda_view.dart', ).readAsStringSync(); - expect(agenda, contains('isTask ? YaruIcons.task_list')); + expect(agenda, contains('YaruIcons.task_list')); + expect(agenda, contains('BusyMaxGlyphs.subdirectoryFor')); + expect(agenda, contains('Icons.account_tree_outlined')); expect(agenda, contains('YaruCheckbox(')); expect(agenda, isNot(contains('selectedColor:'))); expect(agenda, isNot(contains('checkmarkColor:'))); @@ -3868,7 +4176,7 @@ List _itemsFor(DateTime day) { CalendarScheduleItem( id: 'event:1', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Design review', @@ -3881,7 +4189,7 @@ List _itemsFor(DateTime day) { TaskScheduleItem( id: 'task:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Submit report', completed: false, @@ -3900,7 +4208,7 @@ List _sameSlotItemsFor(DateTime day) { CalendarScheduleItem( id: 'event:1', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Design review', @@ -3913,7 +4221,7 @@ List _sameSlotItemsFor(DateTime day) { CalendarScheduleItem( id: 'event:2', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'calendar:primary', providerCalendarId: 'primary', title: 'Pairing session', @@ -3926,7 +4234,7 @@ List _sameSlotItemsFor(DateTime day) { TaskScheduleItem( id: 'task:1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'Submit report', completed: false, @@ -3938,7 +4246,7 @@ List _sameSlotItemsFor(DateTime day) { TaskScheduleItem( id: 'task:2', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'tasks:inbox', title: 'Review notes', completed: false, @@ -3958,7 +4266,7 @@ List _manyAllDayItemsFor(DateTime day) { TaskScheduleItem( id: 'all-day-task:$index', accountId: index.isEven ? 'google:g' : 'microsoft:m', - provider: index.isEven ? TaskProvider.google : TaskProvider.microsoft, + provider: index.isEven ? BusyProvider.google : BusyProvider.microsoft, sourceId: 'tasks:inbox', title: 'All-day task ${index + 1}', completed: false, diff --git a/test/features/schedule/presentation/schedule_workspace_task_mutations_test.dart b/test/features/schedule/presentation/schedule_workspace_task_mutations_test.dart index 02dfb9b..60eb9ee 100644 --- a/test/features/schedule/presentation/schedule_workspace_task_mutations_test.dart +++ b/test/features/schedule/presentation/schedule_workspace_task_mutations_test.dart @@ -9,7 +9,7 @@ import 'package:busymax/src/platform/linux_header_bar_service.dart'; import 'package:busymax/src/platform/native_dialog_service.dart'; import 'package:busymax/src/platform/native_menu_service.dart'; import 'package:busymax/src/schedule/schedule_scope.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -70,6 +70,27 @@ void main() { expect(find.text('Created from Schedule'), findsOneWidget); }); + testWidgets('new task uses the shared provider-qualified account label', ( + tester, + ) async { + await _pumpScheduleWorkspace(tester); + + await tester.tap(find.byTooltip('Create')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Task')); + await tester.pumpAndSettle(); + + final accountRow = tester.widget>( + find.byWidgetPredicate( + (widget) => + widget is BusyMaxComboRow && widget.title == 'Account', + ), + ); + expect(accountRow.selected, _accountId); + expect(accountRow.labelFor(_accountId), 'Google · schedule@example.test'); + expect(accountRow.subtitle, null); + }); + testWidgets('task-list route defaults new tasks to that account and list', ( tester, ) async { @@ -211,7 +232,10 @@ Future<_ScheduleHarness> _pumpScheduleWorkspace( .insert( AccountsCompanion.insert( id: _accountId, - provider: Value(TaskProvider.google.storageValue), + provider: BusyProvider.google.storageValue, + authority: 'https://accounts.google.com', + providerAccountId: _accountId, + credentialKind: 'oauth', displayName: const Value('Schedule test'), authState: const Value(accountAuthStateSignedIn), createdAtUtc: _nowUtc, @@ -256,9 +280,12 @@ Future<_ScheduleHarness> _pumpScheduleWorkspace( final account = AccountEntity( id: _accountId, - provider: TaskProvider.google, + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: _accountId, authState: accountAuthStateSignedIn, displayName: 'Schedule test', + email: 'schedule@example.test', ); final effectiveHeaderBarService = headerBarService ?? LinuxHeaderBarService(isLinux: false); diff --git a/test/features/schedule/schedule_search_test.dart b/test/features/schedule/schedule_search_test.dart index 5b64ead..eea6c09 100644 --- a/test/features/schedule/schedule_search_test.dart +++ b/test/features/schedule/schedule_search_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/calendar_providers/calendar_sync_dto.dart'; import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; @@ -5,7 +7,7 @@ import 'package:busymax/src/schedule/schedule_item.dart'; import 'package:busymax/src/schedule/schedule_filters.dart'; import 'package:busymax/src/schedule/schedule_range.dart'; import 'package:busymax/src/schedule/schedule_repository.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,7 +19,7 @@ void main() { final item = CalendarScheduleItem( id: 'event-1', accountId: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, sourceId: 'cal-1', providerCalendarId: 'cal-1', title: 'Design review', @@ -42,7 +44,7 @@ void main() { const item = TaskScheduleItem( id: 'task-1', accountId: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, sourceId: 'list-1', title: 'Submit report', completed: false, @@ -86,7 +88,7 @@ void main() { () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -94,7 +96,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'calendar', summary: 'Work', ), @@ -102,7 +104,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'calendar', providerEventId: 'event', title: 'Company holiday', @@ -139,7 +141,7 @@ void main() { test('Microsoft timed calendar event keeps time zones for editing', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -147,7 +149,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'calendar', summary: 'Work', ), @@ -155,7 +157,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'calendar', providerEventId: 'event', providerRecurringEventId: 'series-master', @@ -187,7 +189,7 @@ void main() { test('Google RFC3339 offsets convert to local display time', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -195,7 +197,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', summary: 'Work', ), @@ -203,7 +205,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', providerEventId: 'event', title: 'Planning', @@ -237,7 +239,7 @@ void main() { test('UTC calendar instants convert to local display time', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -245,7 +247,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', summary: 'Work', ), @@ -253,7 +255,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', providerEventId: 'event', title: 'Planning', @@ -281,7 +283,7 @@ void main() { test('calendar event keeps recurrence and attendees for editing', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -289,7 +291,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', summary: 'Work', ), @@ -297,7 +299,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', providerEventId: 'event', title: 'Weekly planning', @@ -336,7 +338,7 @@ void main() { () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); final calendarRepository = CalendarRepository( database: database, now: () => DateTime.utc(2026, 6, 9), @@ -344,7 +346,7 @@ void main() { await calendarRepository.upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', summary: 'Work', rawJson: { @@ -358,7 +360,7 @@ void main() { await calendarRepository.upsertEvent( accountId: 'account', event: const CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'calendar', providerEventId: 'event', title: 'Planning', @@ -385,7 +387,7 @@ void main() { test('Microsoft task with start and due appears on start day', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); await _insertTaskList(database); await database .into(database.tasks) @@ -432,7 +434,7 @@ void main() { test('Microsoft task with midnight due appears as timed slot', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); await _insertTaskList(database); await database .into(database.tasks) @@ -475,7 +477,7 @@ void main() { test('Microsoft UTC task reminder appears as local display time', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); await _insertTaskList(database); await database .into(database.tasks) @@ -512,7 +514,7 @@ void main() { test('Microsoft task with date-only due appears as all-day', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.microsoft); + await _insertScheduleAccount(database, provider: BusyProvider.microsoft); await _insertTaskList(database); await database .into(database.tasks) @@ -550,7 +552,7 @@ void main() { test('repository limits no-date task bucket', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); await _insertTaskList(database); for (var index = 0; index < 10; index += 1) { await _insertTask( @@ -592,7 +594,7 @@ void main() { test('repository limits overdue task bucket', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); await _insertTaskList(database); for (var index = 0; index < 10; index += 1) { final due = DateTime(2026, 6, 9).subtract(Duration(days: index)); @@ -663,7 +665,10 @@ void main() { .insert( AccountsCompanion.insert( id: accountId, - provider: const Value('google'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: accountId, + credentialKind: 'oauth', authState: const Value('signed_in'), createdAtUtc: _now, updatedAtUtc: _now, @@ -719,7 +724,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'account-a', - provider: const Value('google'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'account-a', + credentialKind: 'oauth', authState: const Value('signed_in'), createdAtUtc: _now, updatedAtUtc: _now, @@ -787,7 +795,7 @@ void main() { test('repository hides unavailable tasks', () async { final database = AppDatabase(NativeDatabase.memory()); addTearDown(database.close); - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); await _insertTaskList(database); await _insertTask( database, @@ -864,10 +872,304 @@ void main() { expect(noDateItems.items.map((item) => item.title), ['Visible no date']); expect(overdueItems.items.map((item) => item.title), ['Visible overdue']); }); + + test( + 'DAV task selection and cached connection states govern visibility', + () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + await _insertDavScheduleFixture( + database, + authState: 'reauth_required', + tasksSelected: false, + ); + await _insertDavTask( + database, + id: 'cached-task', + title: 'Cached task', + dueUtc: '2026-06-12', + providerMetadata: { + 'nativeDue': {'raw': '20260612', 'kind': 'date'}, + }, + ); + + final repository = ScheduleRepository(database); + const filters = ScheduleFilters( + accountIds: {'dav-account'}, + includeCalendarEvents: false, + showNoDateTasks: false, + ); + expect( + await repository.listItems( + range: ScheduleRange.day(DateTime(2026, 6, 12)), + filters: filters, + ), + isEmpty, + ); + + await (database.update(database.davCollections) + ..where((row) => row.id.equals('dav-collection'))) + .write(const DavCollectionsCompanion(tasksSelected: Value(true))); + final visible = await repository.listItems( + range: ScheduleRange.day(DateTime(2026, 6, 12)), + filters: filters, + ); + expect(visible.map((item) => item.title), ['Cached task']); + expect(visible.single.capabilities.canEdit, isTrue); + expect( + await repository.findTaskTarget( + accountId: 'dav-account', + taskListId: 'dav-list', + taskId: 'cached-task', + ), + const ScheduleTaskTarget( + accountId: 'dav-account', + taskListId: 'dav-list', + taskId: 'cached-task', + ), + ); + + await (database.update( + database.davCollections, + )..where((row) => row.id.equals('dav-collection'))).write( + const DavCollectionsCompanion( + currentUserPrivilegesJson: Value('["{DAV:}read"]'), + readOnly: Value(true), + ), + ); + final readOnly = await repository.listItems( + range: ScheduleRange.day(DateTime(2026, 6, 12)), + filters: filters, + ); + expect(readOnly.single.capabilities.canEdit, isFalse); + expect(readOnly.single.capabilities.canDelete, isFalse); + }, + ); + + test('DAV DTSTART retains timed task scheduling semantics', () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + await _insertDavScheduleFixture( + database, + authState: 'temporarily_unavailable', + ); + await _insertDavTask( + database, + id: 'timed-task', + title: 'Timed DAV task', + dueUtc: '2026-06-12T17:00:00', + providerMetadata: { + 'nativeStart': {'raw': '20260611T093000', 'kind': 'floatingDateTime'}, + 'nativeDue': {'raw': '20260612T170000', 'kind': 'floatingDateTime'}, + }, + ); + + final repository = ScheduleRepository(database); + const filters = ScheduleFilters( + accountIds: {'dav-account'}, + includeCalendarEvents: false, + showNoDateTasks: false, + ); + final startDay = await repository.listItems( + range: ScheduleRange.day(DateTime(2026, 6, 11)), + filters: filters, + ); + final dueDay = await repository.listItems( + range: ScheduleRange.day(DateTime(2026, 6, 12)), + filters: filters, + ); + + expect(startDay, hasLength(1)); + final task = startDay.single as TaskScheduleItem; + expect(task.start, DateTime(2026, 6, 11, 9, 30)); + expect(task.end, DateTime(2026, 6, 11, 10)); + expect(task.allDay, isFalse); + expect(dueDay, isEmpty); + }); + + test('no-date task bucket resolves and orders Google hierarchy', () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + await _insertScheduleAccount(database, provider: BusyProvider.google); + await _insertTaskList(database); + await _insertTask(database, id: 'child', title: 'Child', parent: 'parent'); + await _insertTask(database, id: 'parent', title: 'Parent'); + + final page = await ScheduleRepository(database).listNoDateTasks( + limit: 10, + filters: const ScheduleFilters(accountIds: {'account'}), + ); + + expect(page.items.map((item) => item.id), ['parent', 'child']); + expect(page.items.first.hasSubtasks, isTrue); + expect(page.items.last.parentId, 'parent'); + expect(page.items.last.parentTitle, 'Parent'); + expect(page.items.last.hierarchyDepth, 1); + }); + + test( + 'agenda ancestor closure restores a parent outside a bounded bucket', + () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + await _insertDavScheduleFixture(database, authState: 'signed_in'); + for (var index = 0; index < 8; index += 1) { + await _insertDavTask( + database, + id: 'older-$index', + title: 'Older $index', + dueUtc: '2026-06-0${index + 1}', + providerMetadata: { + 'nativeDue': {'raw': '2026060${index + 1}', 'kind': 'date'}, + }, + ); + } + await _insertDavTask( + database, + id: 'parent-object', + title: 'Parent', + dueUtc: '2026-06-09', + icalUid: 'parent-uid', + providerMetadata: const { + 'nativeDue': {'raw': '20260609', 'kind': 'date'}, + }, + ); + await _insertDavTask( + database, + id: 'child-object', + title: 'Child', + parentUid: 'parent-uid', + ); + + final repository = ScheduleRepository(database); + final filters = ScheduleFilters( + accountIds: {'dav-account'}, + taskListFilterActive: true, + taskListKeys: { + ScheduleTaskListKey(accountId: 'dav-account', taskListId: 'dav-list'), + }, + ); + final overdue = await repository.listOverdueTasks( + before: DateTime(2026, 6, 10), + limit: 8, + filters: filters, + ); + final noDate = await repository.listNoDateTasks( + limit: 8, + filters: filters, + ); + final boundedItems = [...overdue.items, ...noDate.items]; + + expect(overdue.hasMore, isTrue); + expect( + boundedItems.map((item) => item.id), + isNot(contains('parent-object')), + ); + expect(boundedItems.map((item) => item.id), contains('child-object')); + + final agendaItems = await repository.includeTaskAncestors( + boundedItems, + filters: filters, + ); + final child = agendaItems.whereType().singleWhere( + (item) => item.id == 'child-object', + ); + final parent = agendaItems.whereType().singleWhere( + (item) => item.id == 'parent-object', + ); + expect(child.parentId, parent.id); + expect(parent.hasSubtasks, isTrue); + }, + ); +} + +Future _insertDavScheduleFixture( + AppDatabase database, { + required String authState, + bool tasksSelected = true, +}) async { + await database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: 'dav-account', + provider: BusyProvider.nextcloud.storageValue, + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + authState: Value(authState), + createdAtUtc: _now, + updatedAtUtc: _now, + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'dav-collection', + accountId: 'dav-account', + hrefKey: '/remote.php/dav/calendars/alex/tasks/', + requestUri: + 'https://cloud.example.test/remote.php/dav/calendars/alex/tasks/', + displayName: 'Tasks', + supportedComponentMask: const Value(2), + currentUserPrivilegesJson: const Value( + '["{DAV:}read","{DAV:}write"]', + ), + readOnly: const Value(false), + taskProjectionEnabled: const Value(true), + tasksSelected: Value(tasksSelected), + createdAtUtc: _now, + updatedAtUtc: _now, + ), + ); + await database + .into(database.taskLists) + .insert( + TaskListsCompanion.insert( + accountId: 'dav-account', + id: 'dav-list', + davCollectionId: const Value('dav-collection'), + title: 'Tasks', + rawJson: '{}', + createdLocalAtUtc: _now, + updatedLocalAtUtc: _now, + ), + ); +} + +Future _insertDavTask( + AppDatabase database, { + required String id, + required String title, + String? dueUtc, + Map providerMetadata = const {}, + String? icalUid, + String? parentUid, +}) { + return database + .into(database.tasks) + .insert( + TasksCompanion.insert( + accountId: 'dav-account', + taskListId: 'dav-list', + id: id, + davCollectionId: const Value('dav-collection'), + title: title, + status: const Value('needsAction'), + dueUtc: Value(dueUtc), + providerMetadataJson: Value(jsonEncode(providerMetadata)), + icalUid: Value(icalUid), + parentUid: Value(parentUid), + rawJson: '{}', + createdLocalAtUtc: _now, + updatedLocalAtUtc: _now, + ), + ); } Future _seedSearchDatabase(AppDatabase database) async { - await _insertScheduleAccount(database, provider: TaskProvider.google); + await _insertScheduleAccount(database, provider: BusyProvider.google); await _insertTaskList(database); await database .into(database.tasks) @@ -888,14 +1190,19 @@ Future _seedSearchDatabase(AppDatabase database) async { Future _insertScheduleAccount( AppDatabase database, { - required TaskProvider provider, + required BusyProvider provider, }) { return database .into(database.accounts) .insert( AccountsCompanion.insert( id: 'account', - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: 'account', + credentialKind: 'oauth', displayName: const Value('Ada Lovelace'), email: const Value('ada@example.com'), authState: const Value('signed_in'), @@ -928,6 +1235,7 @@ Future _insertTask( bool serverMissing = false, bool? deleted, bool? hidden, + String? parent, }) { return database .into(database.tasks) @@ -942,6 +1250,7 @@ Future _insertTask( serverMissing: Value(serverMissing), deleted: Value(deleted), hidden: Value(hidden), + parent: Value(parent), rawJson: '{}', createdLocalAtUtc: _now, updatedLocalAtUtc: _now, diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index 597afbb..ce9d676 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -11,6 +11,11 @@ import 'package:busymax/src/app/app_bootstrap.dart'; import 'package:busymax/src/app/busymax_design.dart'; import 'package:busymax/src/app/busymax_yaru_theme.dart'; import 'package:busymax/src/config/build_config.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/dav/auth/dav_account_onboarding_service.dart'; +import 'package:busymax/src/dav/auth/nextcloud_login_flow_v2.dart'; +import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; import 'package:busymax/src/features/auth/data/auth_repository.dart'; import 'package:busymax/src/features/settings/presentation/settings_screen.dart'; @@ -20,8 +25,11 @@ import 'package:busymax/src/platform/linux_header_bar_service.dart'; import 'package:busymax/src/platform/native_menu_service.dart'; import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; import 'package:busymax/src/features/tasks/presentation/desktop_date_time_fields.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:busymax/l10n/generated/app_localizations.dart'; +import 'package:drift/native.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:ubuntu_localizations/ubuntu_localizations.dart'; import '../../../test_localized_app.dart'; @@ -111,6 +119,84 @@ void main() { expect(container.read(selectedAccountIdProvider), 'microsoft:m'); }); + testWidgets( + 'Settings reports failed Nextcloud revocation after complete local removal', + (tester) async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + final secrets = InMemorySecretStore(); + await AccountsRepository(database: database).upsertSignedInAccount( + id: _nextcloudAccount.id, + provider: BusyProvider.nextcloud, + authority: _nextcloudAccount.authority, + providerAccountId: _nextcloudAccount.providerAccountId, + credentialKind: CredentialKind.nextcloudAppPassword, + displayName: _nextcloudAccount.displayName, + grantedScopes: '', + ); + await secrets.saveCredential( + _nextcloudAccount.id, + NextcloudSecretRecord( + canonicalServer: Uri.parse(_nextcloudAccount.authority), + loginName: _nextcloudAccount.providerAccountId, + appPassword: 'test-app-password', + ), + ); + await secrets.setActiveAccountId(_nextcloudAccount.id); + final onboarding = DavAccountOnboardingService( + database: database, + secretStore: secrets, + nextcloudLoginFlow: _unusedNextcloudLoginFlow(), + discover: + ({ + required accountId, + required provider, + required accountAuthority, + required credential, + cancellationToken, + }) async => throw StateError('Discovery is not used by removal.'), + nextcloudCredentialRevoker: + ({required accountId, required credential}) async { + throw const DavException( + kind: DavErrorKind.network, + code: 'RevocationOffline', + safeMessage: 'Offline.', + ); + }, + ); + final auth = _FakeAuthRepository(); + final container = _container( + selectedAccountId: _nextcloudAccount.id, + authRepository: auth, + accounts: const [_googleAccount, _nextcloudAccount], + davOnboardingService: onboarding, + ); + addTearDown(container.dispose); + + await _pumpSettings(tester, container); + await _openAccountRemovalDialog(tester); + expect( + find.byKey(const Key('revoke-google-authorization')), + findsNothing, + ); + await tester.tap(find.byKey(const Key('confirm-account-removal'))); + await tester.pumpAndSettle(); + + expect( + find.text( + 'The account was removed locally, but its Nextcloud app password ' + 'could not be revoked.', + ), + findsOneWidget, + ); + expect(auth.removalCalls, isEmpty); + expect(container.read(selectedAccountIdProvider), _googleAccount.id); + expect(await database.select(database.accounts).get(), isEmpty); + expect(await secrets.readCredential(_nextcloudAccount.id), isNull); + expect(await secrets.readActiveAccountId(), isNull); + }, + ); + testWidgets('Settings cancels account removal without mutation', ( tester, ) async { @@ -257,7 +343,10 @@ void main() { await _pumpSettings(tester, container); expect(find.text(accountReconnectRequiredActionLabel), findsOneWidget); - expect(find.text(accountReconnectRequiredSyncMessage), findsOneWidget); + expect( + find.text('Reconnect this account to resume synchronization.'), + findsOneWidget, + ); expect(find.text('New task list'), findsNothing); expect(find.text('Remove account…'), findsOneWidget); @@ -697,7 +786,10 @@ Finder _settingsMenuItemWithLabel(String label) { } Future _openAccountRemovalDialog(WidgetTester tester) async { - await tester.tap(find.text('Remove account…').first); + final removeAction = find.text('Remove account…').first; + await tester.ensureVisible(removeAction); + await tester.pumpAndSettle(); + await tester.tap(removeAction); await tester.pumpAndSettle(); expect(find.textContaining('from BusyMax?'), findsOneWidget); } @@ -710,10 +802,15 @@ ProviderContainer _container({ Map? taskListRepositories, String? activeAccountIdOverride = _useDefaultActiveAccountId, bool useFlutterHeader = false, + DavAccountOnboardingService? davOnboardingService, }) { return ProviderContainer( overrides: [ authRepositoryProvider.overrideWithValue(authRepository), + if (davOnboardingService != null) + davAccountOnboardingServiceProvider.overrideWithValue( + davOnboardingService, + ), accountsRepositoryProvider.overrideWithValue( _FakeAccountsRepository(accounts), ), @@ -721,6 +818,10 @@ ProviderContainer _container({ accountManagementStreamProvider.overrideWith( (ref) => Stream.value(accounts), ), + davCollectionsStreamProvider.overrideWith( + (ref) => Stream.value(const []), + ), + davConflictsStreamProvider.overrideWith((ref) => Stream.value(const [])), selectedAccountIdProvider.overrideWith((ref) => selectedAccountId), if (activeAccountIdOverride != _useDefaultActiveAccountId) activeAccountProvider.overrideWithValue(activeAccountIdOverride), @@ -742,6 +843,11 @@ ProviderContainer _container({ const _useDefaultActiveAccountId = '__busymax_default_active_account__'; +NextcloudLoginFlowV2 _unusedNextcloudLoginFlow() => NextcloudLoginFlowV2( + client: MockClient((_) async => http.Response('', 500)), + browserLauncher: (_) async => false, +); + Future _pumpSettings( WidgetTester tester, ProviderContainer container, { @@ -862,6 +968,9 @@ class _FakeAccountsRepository implements AccountsRepository { final List accounts; + @override + Future> listVisibleAccounts() async => accounts; + @override Future> listSignedInAccounts() async => accounts; @@ -895,7 +1004,9 @@ class _MemorySettingsStore implements LocalSettingsStore { const _googleAccount = AccountEntity( id: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: 'g', displayName: 'Google User', email: 'google@example.com', authState: 'signed_in', @@ -903,15 +1014,29 @@ const _googleAccount = AccountEntity( const _microsoftAccount = AccountEntity( id: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: 'm', displayName: 'Microsoft User', email: 'microsoft@example.com', authState: 'signed_in', ); +const _nextcloudAccount = AccountEntity( + id: 'nextcloud:n', + provider: BusyProvider.nextcloud, + authority: 'https://cloud.example.test', + providerAccountId: 'alex', + credentialKind: CredentialKind.nextcloudAppPassword, + displayName: 'Nextcloud User', + authState: accountAuthStateSignedIn, +); + const _reconnectRequiredGoogleAccount = AccountEntity( id: 'google:g', - provider: TaskProvider.google, + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: 'g', displayName: 'Google User', email: 'google@example.com', authState: accountAuthStateReauthRequired, diff --git a/test/features/sync/calendar_event_clear_patch_test.dart b/test/features/sync/calendar_event_clear_patch_test.dart index 8ae3608..34aaf24 100644 --- a/test/features/sync/calendar_event_clear_patch_test.dart +++ b/test/features/sync/calendar_event_clear_patch_test.dart @@ -8,7 +8,7 @@ import 'package:busymax/src/features/calendar/presentation/event_editor_draft.da import 'package:busymax/src/features/sync/calendar_pending_ops_replayer.dart'; import 'package:busymax/src/google_calendar/google_calendar_api_client.dart'; import 'package:busymax/src/microsoft_calendar/microsoft_calendar_api_client.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -16,7 +16,7 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; void main() { - for (final provider in [TaskProvider.google, TaskProvider.microsoft]) { + for (final provider in [BusyProvider.google, BusyProvider.microsoft]) { test( '${provider.storageValue} event edit explicitly clears recurrence only', () async { @@ -31,7 +31,7 @@ void main() { expect(result.patchRequest.method, 'PATCH'); expect( result.patchRequest.url.path, - provider == TaskProvider.google + provider == BusyProvider.google ? '/calendar/v3/calendars/cal-1/events/event-1' : '/v1.0/me/calendars/cal-1/events/event-1', ); @@ -40,7 +40,7 @@ void main() { expect(body, contains('recurrence')); expect( body['recurrence'], - provider == TaskProvider.google ? [] : null, + provider == BusyProvider.google ? [] : null, ); expect(body, isNot(contains('attendees'))); _expectUnrelatedOptionalFieldsOmitted(body); @@ -82,7 +82,7 @@ Future< ({int applied, Map queuedRequest, http.Request patchRequest}) > _editAndReplay({ - required TaskProvider provider, + required BusyProvider provider, bool clearRecurrence = false, bool clearAttendees = false, }) async { @@ -93,7 +93,12 @@ _editAndReplay({ .insert( AccountsCompanion.insert( id: 'account', - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: 'account', + credentialKind: 'oauth', authState: const Value('signed_in'), createdAtUtc: '2026-06-08T00:00:00.000Z', updatedAtUtc: '2026-06-08T00:00:00.000Z', @@ -112,7 +117,7 @@ _editAndReplay({ timeZone: 'UTC', ), ); - final recurrence = provider == TaskProvider.google + final recurrence = provider == BusyProvider.google ? ['RRULE:FREQ=WEEKLY'] : { 'pattern': { @@ -126,7 +131,7 @@ _editAndReplay({ 'recurrenceTimeZone': 'UTC', }, }; - final attendees = provider == TaskProvider.google + final attendees = provider == BusyProvider.google ? [ {'email': 'guest@example.com', 'displayName': 'Guest'}, ] @@ -214,7 +219,7 @@ _editAndReplay({ ); } - final CloudCalendarClient client = provider == TaskProvider.google + final CloudCalendarClient client = provider == BusyProvider.google ? GoogleCalendarApiClient( httpClient: MockClient(handler), baseUri: Uri.parse('https://www.googleapis.com'), @@ -241,12 +246,12 @@ _editAndReplay({ } Map _eventJson( - TaskProvider provider, { + BusyProvider provider, { required bool edited, required bool includeRecurrence, required bool includeAttendees, }) { - return provider == TaskProvider.google + return provider == BusyProvider.google ? _googleEventJson( edited: edited, includeRecurrence: includeRecurrence, diff --git a/test/features/sync/calendar_pending_ops_replayer_test.dart b/test/features/sync/calendar_pending_ops_replayer_test.dart index 155467d..132034e 100644 --- a/test/features/sync/calendar_pending_ops_replayer_test.dart +++ b/test/features/sync/calendar_pending_ops_replayer_test.dart @@ -10,7 +10,7 @@ import 'package:busymax/src/features/calendar/presentation/event_editor_draft.da import 'package:busymax/src/features/sync/calendar_pending_ops_replayer.dart'; import 'package:busymax/src/features/sync/calendar_sync_engine.dart'; import 'package:busymax/src/google_calendar/google_calendar_errors.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -26,7 +26,7 @@ void main() { await CalendarRepository(database: database).upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', timeZone: 'America/Vancouver', @@ -79,7 +79,7 @@ void main() { await CalendarRepository(database: database).upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', timeZone: 'UTC', @@ -116,7 +116,7 @@ void main() { await CalendarRepository(database: database).upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', timeZone: 'UTC', @@ -171,7 +171,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'microsoft-account', - provider: const Value('microsoft'), + provider: 'microsoft', + authority: 'https://login.microsoftonline.com/common', + providerAccountId: 'microsoft-account', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', @@ -181,7 +184,7 @@ void main() { await CalendarRepository(database: database).upsertSource( accountId: 'account', source: const CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-2', summary: 'Personal', timeZone: 'America/Vancouver', @@ -190,7 +193,7 @@ void main() { await CalendarRepository(database: database).upsertSource( accountId: 'microsoft-account', source: const CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'ms-cal-1', summary: 'Outlook', timeZone: 'America/Vancouver', @@ -871,7 +874,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', - provider: const Value('google'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-06-08T00:00:00.000Z', @@ -888,7 +894,7 @@ Future _insertEvent( String? endTimeZone, }) async { final event = CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', providerEventId: providerEventId, providerRecurringEventId: providerRecurringEventId, @@ -909,7 +915,7 @@ Future _insertEvent( ).upsertEvent(accountId: 'account', event: event); return CalendarRepository.eventId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', providerEventId: providerEventId, ); @@ -964,7 +970,7 @@ class _FakeCalendarClient implements CloudCalendarClient { GoogleCalendarApiError? deleteError; @override - BusyProvider get provider => TaskProvider.google; + BusyProvider get provider => BusyProvider.google; @override CalendarProviderCapabilities get capabilities => @@ -975,7 +981,7 @@ class _FakeCalendarClient implements CloudCalendarClient { calls.add('listCalendars'); return const [ CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ), @@ -1090,7 +1096,7 @@ class _FakeCalendarClient implements CloudCalendarClient { String? endTimeZone, }) { return CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', providerEventId: id, etagOrChangeKey: etagOrChangeKey, @@ -1115,7 +1121,7 @@ class _FakeCalendarClient implements CloudCalendarClient { Future createCalendar(CalendarMutation mutation) async { calls.add('createCalendar:${mutation.summary}'); return CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-created', summary: mutation.summary ?? 'Calendar', ); @@ -1162,7 +1168,7 @@ class _FakeCalendarClient implements CloudCalendarClient { ) async { calls.add('updateCalendar:$calendarId:${mutation.summary}'); return CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: calendarId, summary: mutation.summary ?? 'Calendar', ); diff --git a/test/features/sync/calendar_sync_engine_test.dart b/test/features/sync/calendar_sync_engine_test.dart index 3824d38..8564853 100644 --- a/test/features/sync/calendar_sync_engine_test.dart +++ b/test/features/sync/calendar_sync_engine_test.dart @@ -5,7 +5,7 @@ import 'package:busymax/src/calendar_providers/cloud_calendar_client.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/calendar/data/calendar_repository.dart'; import 'package:busymax/src/features/sync/calendar_sync_engine.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -23,31 +23,33 @@ void main() { test('same-month sync reuses its cursor and one sync-state row', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', primaryCalendar: true, ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); await _insertSource(database, source); await database - .into(database.calendarSyncStates) + .into(database.syncCursors) .insert( - CalendarSyncStatesCompanion.insert( + SyncCursorsCompanion.insert( id: 'account|google|events|account|google|cal-1|' '2025-07-01T00:00:00.000Z|2028-08-01T00:00:00.000Z', accountId: 'account', provider: 'google', - syncKind: 'events', - calendarSourceId: const Value('account|google|cal-1'), + transport: 'rest', + syncScopeKind: 'events', + cursorKind: 'google_sync_token', + cursorValue: 'legacy-token', + projectionSourceId: const Value('account|google|cal-1'), rangeStart: const Value('2025-07-01T00:00:00.000Z'), rangeEnd: const Value('2028-08-01T00:00:00.000Z'), - googleSyncToken: const Value('legacy-token'), ), ); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -81,23 +83,24 @@ void main() { expect(call.rangeEnd, DateTime.utc(2028, 8)); expect(call.primaryCalendar, isTrue); } - final states = await database.select(database.calendarSyncStates).get(); + final states = await database.select(database.syncCursors).get(); expect(states, hasLength(1)); expect(states.single.id, 'account|google|events|account|google|cal-1'); - expect(states.single.googleSyncToken, 'google-token-2'); + expect(states.single.cursorKind, 'google_sync_token'); + expect(states.single.cursorValue, 'google-token-2'); expect(states.single.rangeStart, '2025-07-01T00:00:00.000Z'); expect(states.single.rangeEnd, '2028-08-01T00:00:00.000Z'); }); test('a new month rebases the cursor without adding a state row', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -130,9 +133,9 @@ void main() { expect(client.syncCalls[0].rangeEnd, DateTime.utc(2028, 8)); expect(client.syncCalls[1].rangeStart, DateTime.utc(2025, 8)); expect(client.syncCalls[1].rangeEnd, DateTime.utc(2028, 9)); - final states = await database.select(database.calendarSyncStates).get(); + final states = await database.select(database.syncCursors).get(); expect(states, hasLength(1)); - expect(states.single.googleSyncToken, 'google-token-august'); + expect(states.single.cursorValue, 'google-token-august'); expect(states.single.rangeStart, '2025-08-01T00:00:00.000Z'); expect(states.single.rangeEnd, '2028-09-01T00:00:00.000Z'); }); @@ -141,29 +144,30 @@ void main() { 'legacy Google state without expansion marker establishes a new baseline', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); await _insertSource(database, source); final repository = CalendarRepository(database: database); await repository.saveSyncState( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, syncKind: 'events', calendarSourceId: CalendarRepository.sourceId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ), rangeStart: '2025-07-01T00:00:00.000Z', rangeEnd: '2028-08-01T00:00:00.000Z', - googleSyncToken: 'legacy-unexpanded-token', + cursorKind: 'google_sync_token', + cursorValue: 'legacy-unexpanded-token', full: true, ); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -192,17 +196,17 @@ void main() { ]); final state = await repository.syncState( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, syncKind: 'events', calendarSourceId: CalendarRepository.sourceId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ), ); expect(state, isNot(equals(null))); - expect(state!.googleSyncToken, 'expanded-token-2'); - expect(state.rawStateJson, _expandedGoogleState); + expect(state!.cursorValue, 'expanded-token-2'); + expect(state.stateJson, _expandedGoogleState); }, ); @@ -210,15 +214,15 @@ void main() { 'incremental Google instances retire their synchronized recurrence master', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); await _insertSource(database, source); final repository = CalendarRepository(database: database); const master = CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', providerEventId: 'series-1', title: 'Weekly planning', @@ -235,21 +239,22 @@ void main() { await repository.upsertEvent(accountId: 'account', event: master); await repository.saveSyncState( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, syncKind: 'events', calendarSourceId: CalendarRepository.sourceId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ), rangeStart: '2025-07-01T00:00:00.000Z', rangeEnd: '2028-08-01T00:00:00.000Z', - googleSyncToken: 'expanded-token-1', - rawStateJson: _expandedGoogleState, + cursorKind: 'google_sync_token', + cursorValue: 'expanded-token-1', + stateJson: _expandedGoogleState, full: true, ); const instance = CalendarEventDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', providerEventId: 'instance-1', providerRecurringEventId: 'series-1', @@ -265,7 +270,7 @@ void main() { }, ); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -285,13 +290,13 @@ void main() { expect(client.syncCalls.single.syncTokenOrDeltaLink, 'expanded-token-1'); final masterId = CalendarRepository.eventId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, providerEventId: master.providerEventId, ); final instanceId = CalendarRepository.eventId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, providerEventId: instance.providerEventId, providerOriginalStartKey: instance.providerOriginalStartKey, @@ -307,19 +312,19 @@ void main() { test('no-cursor baseline reconciles a missing provider event', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); await _insertSource(database, source); final eventId = await _insertEvent( database, - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -344,34 +349,35 @@ void main() { test('empty cursor delta does not delete unchanged local events', () async { const source = CalendarSourceDto( - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: 'cal-1', summary: 'Work', ); - await _insertAccount(database, provider: TaskProvider.google); + await _insertAccount(database, provider: BusyProvider.google); await _insertSource(database, source); final eventId = await _insertEvent( database, - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ); await CalendarRepository(database: database).saveSyncState( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, syncKind: 'events', calendarSourceId: CalendarRepository.sourceId( accountId: 'account', - provider: TaskProvider.google, + provider: BusyProvider.google, providerCalendarId: source.providerCalendarId, ), rangeStart: '2025-07-01T00:00:00.000Z', rangeEnd: '2028-08-01T00:00:00.000Z', - googleSyncToken: 'google-token-1', - rawStateJson: _expandedGoogleState, + cursorKind: 'google_sync_token', + cursorValue: 'google-token-1', + stateJson: _expandedGoogleState, full: true, ); final client = _FakeCalendarClient( - provider: TaskProvider.google, + provider: BusyProvider.google, calendars: const [source], pages: const [ CalendarSyncPageDto( @@ -399,7 +405,7 @@ void main() { 'Microsoft primary sync persists and reuses the terminal delta link', () async { const source = CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'cal-primary', summary: 'Calendar', primaryCalendar: true, @@ -407,9 +413,9 @@ void main() { const nextLink = 'https://graph.example/delta?page=2'; const firstDeltaLink = 'https://graph.example/delta?state=one'; const secondDeltaLink = 'https://graph.example/delta?state=two'; - await _insertAccount(database, provider: TaskProvider.microsoft); + await _insertAccount(database, provider: BusyProvider.microsoft); final client = _FakeCalendarClient( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, calendars: const [source], pages: const [ CalendarSyncPageDto(events: [], nextPageTokenOrUrl: nextLink), @@ -442,9 +448,10 @@ void main() { client.syncCalls.map((call) => call.primaryCalendar), everyElement(isTrue), ); - final states = await database.select(database.calendarSyncStates).get(); + final states = await database.select(database.syncCursors).get(); expect(states, hasLength(1)); - expect(states.single.microsoftDeltaLink, secondDeltaLink); + expect(states.single.cursorKind, 'microsoft_delta_link'); + expect(states.single.cursorValue, secondDeltaLink); }, ); @@ -452,38 +459,39 @@ void main() { 'Microsoft non-primary incremental sync reconciles each full snapshot', () async { const source = CalendarSourceDto( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: 'cal-secondary', summary: 'Shared', primaryCalendar: false, ); - await _insertAccount(database, provider: TaskProvider.microsoft); + await _insertAccount(database, provider: BusyProvider.microsoft); await _insertSource(database, source); final eventId = await _insertEvent( database, - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: source.providerCalendarId, ); await CalendarRepository(database: database).saveSyncState( accountId: 'account', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, syncKind: 'events', calendarSourceId: CalendarRepository.sourceId( accountId: 'account', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: source.providerCalendarId, ), rangeStart: '2025-07-01T00:00:00.000Z', rangeEnd: '2028-08-01T00:00:00.000Z', - microsoftDeltaLink: 'https://graph.example/primary-delta', + cursorKind: 'microsoft_delta_link', + cursorValue: 'https://graph.example/primary-delta', full: true, ); final providerEvent = _event( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, providerCalendarId: source.providerCalendarId, ); final client = _FakeCalendarClient( - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, calendars: const [source], pages: [ CalendarSyncPageDto(events: [providerEvent]), @@ -532,7 +540,12 @@ Future _insertAccount( .insert( AccountsCompanion.insert( id: 'account', - provider: Value(provider.storageValue), + provider: provider.storageValue, + authority: provider == BusyProvider.microsoft + ? 'https://login.microsoftonline.com/common' + : 'https://accounts.google.com', + providerAccountId: 'account', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-07-01T00:00:00.000Z', @@ -616,7 +629,7 @@ class _FakeCalendarClient implements CloudCalendarClient { @override CalendarProviderCapabilities get capabilities => - provider == TaskProvider.microsoft + provider == BusyProvider.microsoft ? microsoftCalendarProviderCapabilities : googleCalendarProviderCapabilities; diff --git a/test/features/sync/pending_mutation_sync_requester_test.dart b/test/features/sync/pending_mutation_sync_requester_test.dart index fa4656f..3f4d1c7 100644 --- a/test/features/sync/pending_mutation_sync_requester_test.dart +++ b/test/features/sync/pending_mutation_sync_requester_test.dart @@ -7,9 +7,9 @@ import 'package:busymax/src/features/sync/pending_mutation_sync_requester.dart'; import 'package:busymax/src/features/sync/sync_auth_error.dart'; import 'package:busymax/src/features/sync/sync_engine.dart'; import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_client.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_client.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; void main() { test('multiple rapid requests produce one sync call', () async { @@ -158,7 +158,7 @@ void main() { addTearDown(database.close); await _insertAccount(database); await database.taskListsDao.upsertTaskList(_taskList()); - final apiClient = _FakeGoogleTasksApiClient() + final apiClient = _FakeTaskRemoteClient() ..taskListsPages = [ TaskListsPageDto(items: [_taskListDto('list-1')], rawJson: const {}), ] @@ -225,6 +225,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), @@ -252,7 +256,7 @@ TaskListDto _taskListDto(String id) { const _now = '2026-06-04T00:00:00.000Z'; -class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { +class _FakeTaskRemoteClient implements TaskRemoteClient { var taskListsPages = []; final taskPages = >{}; final calls = []; diff --git a/test/features/sync/pending_op_resolution_service_test.dart b/test/features/sync/pending_op_resolution_service_test.dart index 7ad12b6..61db065 100644 --- a/test/features/sync/pending_op_resolution_service_test.dart +++ b/test/features/sync/pending_op_resolution_service_test.dart @@ -6,19 +6,19 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/sync/pending_op_resolution_service.dart'; import 'package:busymax/src/features/sync/sync_engine.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_client.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_client.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_error.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; void main() { late AppDatabase database; - late _FakeGoogleTasksApiClient apiClient; + late _FakeTaskRemoteClient apiClient; late _FakeSyncEngine syncEngine; late PendingOpResolutionService service; setUp(() async { database = AppDatabase(NativeDatabase.memory()); - apiClient = _FakeGoogleTasksApiClient(); + apiClient = _FakeTaskRemoteClient(); syncEngine = _FakeSyncEngine(); service = PendingOpResolutionService( database: database, @@ -341,7 +341,7 @@ void main() { }); } -class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { +class _FakeTaskRemoteClient implements TaskRemoteClient { TaskDto? remoteTask; TaskListDto? remoteTaskList; GoogleTasksApiError? getTaskError; @@ -390,6 +390,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), diff --git a/test/features/sync/pending_ops_replayer_test.dart b/test/features/sync/pending_ops_replayer_test.dart index d965a67..efe34ba 100644 --- a/test/features/sync/pending_ops_replayer_test.dart +++ b/test/features/sync/pending_ops_replayer_test.dart @@ -7,21 +7,22 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/sync/pending_ops_replayer.dart'; import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_client.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_client.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_error.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_checklist_item.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_client.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_error.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_models.dart'; -import 'package:busymax/src/microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart'; +import 'package:busymax/src/microsoft_todo/api/microsoft_todo_task_remote_client.dart'; void main() { late AppDatabase database; - late _FakeGoogleTasksApiClient apiClient; + late _FakeTaskRemoteClient apiClient; setUp(() async { database = AppDatabase(NativeDatabase.memory()); - apiClient = _FakeGoogleTasksApiClient(); + apiClient = _FakeTaskRemoteClient(); await _insertAccount(database); await database.taskListsDao.upsertTaskList(_taskList('list-1')); }); @@ -153,6 +154,98 @@ void main() { expect(lists.map((list) => list.id), isNot(contains('local-tasklist-1'))); }); + test( + 'Google subtask is moved under its parent after a root insert', + () async { + await database.tasksDao.upsertTask(_task('list-1', 'parent')); + final repository = TasksRepository( + database: database, + accountId: 'account', + apiClient: apiClient, + nowUtc: () => DateTime.utc(2026, 6, 4), + ); + await repository.createSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + title: 'Child', + ); + + final applied = await PendingOpsReplayer( + database: database, + apiClient: apiClient, + accountId: 'account', + random: Random(0), + nowUtc: () => DateTime.utc(2026, 6, 4, 1), + ).replayDueOps(); + + expect(applied, 2); + expect(apiClient.calls, ['create_task:list-1', 'move_task:task-server']); + expect(apiClient.createParentTaskIds, ['parent']); + expect(apiClient.moveParentTaskIds, ['parent']); + expect(await database.select(database.pendingOps).get(), isEmpty); + final tasks = await database.tasksDao.listTasks('account', 'list-1'); + final child = tasks.singleWhere((task) => task.id == 'task-server'); + expect(child.parent, 'parent'); + }, + ); + + test('failed Google subtask move retries without another insert', () async { + await database.tasksDao.upsertTask(_task('list-1', 'parent')); + final repository = TasksRepository( + database: database, + accountId: 'account', + apiClient: apiClient, + nowUtc: () => DateTime.utc(2026, 6, 4), + ); + await repository.createSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + title: 'Child', + ); + apiClient.moveTaskError = const GoogleTasksApiError( + statusCode: 503, + message: 'Temporarily unavailable', + ); + + final firstApplied = await PendingOpsReplayer( + database: database, + apiClient: apiClient, + accountId: 'account', + random: Random(0), + nowUtc: () => DateTime.utc(2026, 6, 4, 1), + ).replayDueOps(); + + expect(firstApplied, 1); + var pending = await database.select(database.pendingOps).get(); + expect(pending, hasLength(1)); + expect(pending.single.operation, 'move_task'); + expect(pending.single.taskId, 'task-server'); + expect(apiClient.calls, ['create_task:list-1', 'move_task:task-server']); + + apiClient.moveTaskError = null; + final secondApplied = await PendingOpsReplayer( + database: database, + apiClient: apiClient, + accountId: 'account', + random: Random(0), + nowUtc: () => DateTime.utc(2026, 6, 4, 2), + ).replayDueOps(); + + expect(secondApplied, 1); + pending = await database.select(database.pendingOps).get(); + expect(pending, isEmpty); + expect(apiClient.calls, [ + 'create_task:list-1', + 'move_task:task-server', + 'move_task:task-server', + ]); + final tasks = await database.tasksDao.listTasks('account', 'list-1'); + expect( + tasks.singleWhere((task) => task.id == 'task-server').parent, + 'parent', + ); + }); + test('404 delete is treated as success', () async { apiClient.deleteTaskError = const GoogleTasksApiError( statusCode: 404, @@ -939,12 +1032,75 @@ void main() { expect(op.lastErrorMessage, contains('Clear completed')); expect(op.nextAttemptAtUtc, startsWith('9999-12-31')); }); + + test( + 'replays checklist create and dependent patch against the server id', + () async { + final checklistClient = _ChecklistTaskRemoteClient(); + await database.tasksDao.upsertTask( + _task( + 'list-1', + 'task-1', + checklistItemsJson: jsonEncode([ + {'id': 'local-step', 'displayName': 'Step', 'isChecked': false}, + ]), + ), + ); + await _enqueue( + database, + id: '01', + operation: 'create_task_checklist_item', + entityType: 'task_checklist_item', + taskListId: 'list-1', + taskId: 'task-1', + localTempId: 'local-step', + request: { + 'checklistItemId': 'local-step', + 'body': {'displayName': 'Step', 'isChecked': false}, + }, + ); + await _enqueue( + database, + id: '02', + operation: 'patch_task_checklist_item', + entityType: 'task_checklist_item', + taskListId: 'list-1', + taskId: 'task-1', + request: { + 'checklistItemId': 'local-step', + 'body': {'isChecked': true}, + }, + ); + + final applied = await PendingOpsReplayer( + database: database, + apiClient: checklistClient, + accountId: 'account', + random: Random(0), + nowUtc: () => DateTime.utc(2026, 6, 4), + ).replayDueOps(); + + final task = (await database.tasksDao.listTasks( + 'account', + 'list-1', + )).single; + final item = decodeTaskChecklistItems( + task.microsoftChecklistItemsJson, + ).single; + expect(applied, 2); + expect(checklistClient.checklistCalls, [ + 'create:Step', + 'update:server-step:true', + ]); + expect(item.id, 'server-step'); + expect(item.completed, isTrue); + expect(await database.select(database.pendingOps).get(), isEmpty); + }, + ); } -MicrosoftTodoGoogleTasksAdapter _microsoftAdapter( - MicrosoftTodoApiClient client, -) { - return MicrosoftTodoGoogleTasksAdapter( +MicrosoftTodoTaskRemoteClient _microsoftAdapter(MicrosoftTodoApiClient client) { + return MicrosoftTodoTaskRemoteClient( client: client, defaultTimeZone: 'UTC', nowUtc: () => DateTime.utc(2026, 6, 4), @@ -990,12 +1146,15 @@ class _ThrowingMicrosoftTodoApiClient implements MicrosoftTodoApiClient { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } -class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { +class _FakeTaskRemoteClient implements TaskRemoteClient { final calls = []; final taskPatchFields = >[]; + final createParentTaskIds = []; + final moveParentTaskIds = []; GoogleTasksApiError? patchTaskListError; GoogleTasksApiError? deleteTaskError; GoogleTasksApiError? clearCompletedError; + GoogleTasksApiError? moveTaskError; TaskListDto? remoteTaskList; TaskDto? remoteTask; bool persistTaskPatches = false; @@ -1046,6 +1205,7 @@ class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { required TaskCreate create, }) async { calls.add('create_task:$taskListId'); + createParentTaskIds.add(parentTaskId); return _taskDto('task-server', title: create.fields['title'].toString()); } @@ -1107,7 +1267,10 @@ class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { String? destinationTaskListId, }) async { calls.add('move_task:$taskId'); - return _taskDto(taskId, title: 'Moved'); + moveParentTaskIds.add(parentTaskId); + final error = moveTaskError; + if (error != null) throw error; + return _taskDto(taskId, title: 'Moved', parent: parentTaskId); } @override @@ -1157,12 +1320,78 @@ class _FakeGoogleTasksApiClient implements GoogleTasksApiClient { } } +class _ChecklistTaskRemoteClient extends _FakeTaskRemoteClient + implements TaskChecklistRemoteClient { + final checklistCalls = []; + + @override + Future createChecklistItem({ + required String taskListId, + required String taskId, + required String title, + bool completed = false, + }) async { + checklistCalls.add('create:$title'); + return TaskChecklistItemDto( + id: 'server-step', + title: title, + completed: completed, + rawJson: { + 'id': 'server-step', + 'displayName': title, + 'isChecked': completed, + }, + ); + } + + @override + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + String? title, + bool? completed, + }) async { + checklistCalls.add('update:$checklistItemId:$completed'); + return TaskChecklistItemDto( + id: checklistItemId, + title: title ?? 'Step', + completed: completed ?? false, + rawJson: { + 'id': checklistItemId, + 'displayName': title ?? 'Step', + 'isChecked': completed ?? false, + }, + ); + } + + @override + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }) async { + checklistCalls.add('delete:$checklistItemId'); + } + + @override + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? pageToken, + }) async => const TaskChecklistItemsPageDto(items: [], rawJson: {}); +} + Future _insertAccount(AppDatabase database) { return database .into(database.accounts) .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), @@ -1192,6 +1421,7 @@ TasksCompanion _task( String title = 'Task', String? updatedUtc, String? rawJson, + String? checklistItemsJson, }) { return TasksCompanion.insert( accountId: 'account', @@ -1200,6 +1430,7 @@ TasksCompanion _task( title: title, updatedUtc: Value(updatedUtc), rawJson: rawJson ?? jsonEncode({'id': id, 'title': title}), + microsoftChecklistItemsJson: Value(checklistItemsJson), localDirty: Value(id.startsWith('local-')), localCreated: Value(id.startsWith('local-')), createdLocalAtUtc: _now, @@ -1262,6 +1493,7 @@ TaskDto _taskDto( String? notes, DateTime? updated, String? status, + String? parent, }) { return TaskDto( id: id, @@ -1269,12 +1501,14 @@ TaskDto _taskDto( notes: notes, updated: updated, status: status, + parent: parent, rawJson: { 'id': id, 'title': title, if (notes != null) 'notes': notes, if (updated != null) 'updated': updated.toIso8601String(), if (status != null) 'status': status, + if (parent != null) 'parent': parent, }, ); } diff --git a/test/features/sync/sync_engine_test.dart b/test/features/sync/sync_engine_test.dart index f3f2540..0232f4e 100644 --- a/test/features/sync/sync_engine_test.dart +++ b/test/features/sync/sync_engine_test.dart @@ -5,17 +5,18 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/sync/sync_engine.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_client.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_client.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_checklist_item.dart'; void main() { late AppDatabase database; - late FakeGoogleTasksApiClient apiClient; + late FakeTaskRemoteClient apiClient; setUp(() async { database = AppDatabase(NativeDatabase.memory()); await _insertAccount(database); - apiClient = FakeGoogleTasksApiClient(); + apiClient = FakeTaskRemoteClient(); }); tearDown(() async { @@ -371,17 +372,127 @@ void main() { expect(op == null, isFalse); expect(op!.attemptCount, 1); }); + + test( + 'sync stores Microsoft checklist children on their parent task', + () async { + apiClient.taskListsPages = [ + TaskListsPageDto(items: [_taskListDto('list-1')], rawJson: const {}), + ]; + apiClient.taskPages['list-1'] = [ + TasksPageDto(items: [_taskDto('task-1')], rawJson: const {}), + ]; + apiClient.checklistPages['list-1/task-1'] = [ + TaskChecklistItemsPageDto( + items: [ + TaskChecklistItemDto( + id: 'step-1', + title: 'First step', + completed: false, + createdAtUtc: DateTime.utc(2026, 6, 1), + rawJson: const { + 'id': 'step-1', + 'displayName': 'First step', + 'isChecked': false, + }, + ), + const TaskChecklistItemDto( + id: 'step-2', + title: 'Second step', + completed: true, + rawJson: { + 'id': 'step-2', + 'displayName': 'Second step', + 'isChecked': true, + }, + ), + ], + rawJson: const {}, + ), + ]; + + await SyncEngine( + database: database, + apiClient: apiClient, + accountId: 'account', + fullRefreshOnly: true, + nowUtc: () => DateTime.utc(2026, 6, 4), + ).fullSync(); + + final task = (await database.tasksDao.listTasks( + 'account', + 'list-1', + )).single; + final checklist = decodeTaskChecklistItems( + task.microsoftChecklistItemsJson, + ); + expect(checklist.map((item) => item.title), [ + 'First step', + 'Second step', + ]); + expect(checklist.last.completed, isTrue); + expect(apiClient.checklistPageTokens['list-1/task-1'], [null]); + }, + ); } -class FakeGoogleTasksApiClient implements GoogleTasksApiClient { +class FakeTaskRemoteClient + implements TaskRemoteClient, TaskChecklistRemoteClient { var taskListsPages = []; final taskPages = >{}; final calls = []; final listPageTokens = []; final taskPageTokens = >{}; + final checklistPages = >{}; + final checklistPageTokens = >{}; DateTime? lastUpdatedMin; var _taskListPageIndex = 0; final _taskPageIndexes = {}; + final _checklistPageIndexes = {}; + + @override + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? pageToken, + }) async { + final key = '$taskListId/$taskId'; + checklistPageTokens.putIfAbsent(key, () => []).add(pageToken); + final pages = checklistPages[key]; + if (pages == null) { + return const TaskChecklistItemsPageDto(items: [], rawJson: {}); + } + final index = _checklistPageIndexes.update( + key, + (value) => value + 1, + ifAbsent: () => 0, + ); + return pages[index]; + } + + @override + Future createChecklistItem({ + required String taskListId, + required String taskId, + required String title, + bool completed = false, + }) => throw UnimplementedError(); + + @override + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + String? title, + bool? completed, + }) => throw UnimplementedError(); + + @override + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }) => throw UnimplementedError(); @override Future listTaskListsPage({ @@ -499,6 +610,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), diff --git a/test/features/sync/sync_scheduler_test.dart b/test/features/sync/sync_scheduler_test.dart index a6fb522..687480d 100644 --- a/test/features/sync/sync_scheduler_test.dart +++ b/test/features/sync/sync_scheduler_test.dart @@ -6,8 +6,8 @@ import 'package:busymax/src/features/sync/all_accounts_sync_scheduler.dart'; import 'package:busymax/src/features/sync/sync_auth_error.dart'; import 'package:busymax/src/features/sync/sync_engine.dart'; import 'package:busymax/src/features/sync/sync_scheduler.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; void main() { test('scheduler reports background sync failure', () async { @@ -154,7 +154,9 @@ Future _waitFor(bool Function() condition) async { AccountEntity _account(String id) { return AccountEntity( id: id, - provider: TaskProvider.google, + provider: BusyProvider.google, + authority: 'https://accounts.google.com', + providerAccountId: id, authState: 'signed_in', ); } diff --git a/test/features/task_lists/data/task_lists_repository_test.dart b/test/features/task_lists/data/task_lists_repository_test.dart index 1954986..465bea7 100644 --- a/test/features/task_lists/data/task_lists_repository_test.dart +++ b/test/features/task_lists/data/task_lists_repository_test.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/dav/mutation/dav_task_list_mutation_service.dart'; import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; void main() { @@ -117,6 +118,69 @@ void main() { expect(lists.map((list) => list.id), ['visible']); expect(watchedLists.map((list) => list.id), ['visible']); }); + + test( + 'Nextcloud list mutations use CalDAV instead of REST pending ops', + () async { + await (database.update( + database.accounts, + )..where((row) => row.id.equals('account'))).write( + const AccountsCompanion( + provider: Value('nextcloud'), + authority: Value('https://cloud.example.test'), + providerAccountId: Value('alex'), + credentialKind: Value('nextcloud_app_password'), + ), + ); + await database + .into(database.davCollections) + .insert( + DavCollectionsCompanion.insert( + id: 'collection', + accountId: 'account', + hrefKey: '/remote.php/dav/calendars/alex/tasks/', + requestUri: + 'https://cloud.example.test/remote.php/dav/calendars/alex/tasks/', + displayName: 'Tasks', + supportedComponentMask: const Value(2), + taskProjectionEnabled: const Value(true), + createdAtUtc: _now, + updatedAtUtc: _now, + ), + ); + await database.taskListsDao.upsertTaskList( + TaskListsCompanion.insert( + accountId: 'account', + id: 'dav-list', + davCollectionId: const Value('collection'), + title: 'Tasks', + rawJson: '{}', + createdLocalAtUtc: _now, + updatedLocalAtUtc: _now, + ), + ); + final dav = _FakeDavTaskListMutations(); + repository = TaskListsRepository( + database: database, + accountId: 'account', + davMutationClient: dav, + ); + + await repository.createTaskList('New list'); + await repository.renameTaskList('dav-list', 'Renamed'); + await repository.deleteTaskList('dav-list'); + + expect(dav.created, ['New list']); + expect(dav.renamed, [('collection', 'Renamed')]); + expect(dav.deleted, ['collection']); + expect(await database.select(database.pendingOps).get(), isEmpty); + expect( + (await database.select(database.taskLists).getSingle()).title, + 'Tasks', + reason: 'DAV projections change only after server-confirmed discovery.', + ); + }, + ); } TaskListsRepository _repository( @@ -137,6 +201,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', createdAtUtc: _now, updatedAtUtc: _now, ), @@ -161,3 +229,22 @@ TaskListsCompanion _taskList( updatedLocalAtUtc: _now, ); } + +final class _FakeDavTaskListMutations implements DavTaskListMutationClient { + final created = []; + final renamed = <(String, String)>[]; + final deleted = []; + + @override + Future createTaskList(String title) async => created.add(title); + + @override + Future deleteTaskList(String collectionId) async { + deleted.add(collectionId); + } + + @override + Future renameTaskList(String collectionId, String title) async { + renamed.add((collectionId, title)); + } +} diff --git a/test/features/tasks/data/tasks_repository_test.dart b/test/features/tasks/data/tasks_repository_test.dart index f16d3cd..4b6b037 100644 --- a/test/features/tasks/data/tasks_repository_test.dart +++ b/test/features/tasks/data/tasks_repository_test.dart @@ -5,6 +5,8 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; +import 'package:busymax/src/features/tasks/domain/task_checklist_item.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_client.dart'; void main() { late AppDatabase database; @@ -60,6 +62,34 @@ void main() { expect(mutationQueuedCalls, 1); }); + test('Google subtask queues a dependent hierarchy move', () async { + await database.tasksDao.upsertTask(_task(id: 'parent', position: '1')); + + await repository.createSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + title: 'Child', + ); + + final tasks = await database.tasksDao.listTasks('account', 'list-1'); + final child = tasks.singleWhere((task) => task.id.startsWith('local-')); + final ops = await database.pendingOpsDao.pendingOpsForReplay( + 'account', + DateTime.utc(2026, 6, 4, 1), + ); + final create = ops.singleWhere((op) => op.operation == 'create_task'); + final move = ops.singleWhere((op) => op.operation == 'move_task'); + + expect(child.parent, 'parent'); + expect(jsonDecode(create.requestJson), { + 'body': {'title': 'Child'}, + 'parent': 'parent', + }); + expect(move.taskId, child.id); + expect(move.dependsOnOpId, create.id); + expect(jsonDecode(move.requestJson), {'parent': 'parent'}); + }); + test('createTask writes and queues extended task fields', () async { await repository.createTask( 'list-1', @@ -290,6 +320,145 @@ void main() { expect(tree.map((node) => node.task.id), ['task-10', 'task-2']); }, ); + + test('watchTaskTree sorts DAV task order numerically', () async { + await database.tasksDao.upsertTask( + _task(id: 'task-2', position: '2', sortOrder: const Value(2)), + ); + await database.tasksDao.upsertTask( + _task(id: 'task-10', position: '10', sortOrder: const Value(10)), + ); + + final tree = await repository + .watchTaskTree('list-1', const TaskViewFilter()) + .first; + + expect(tree.map((node) => node.task.id), ['task-2', 'task-10']); + }); + + test('resolves Google ids and Nextcloud UIDs into task hierarchy', () async { + await database.tasksDao.upsertTask( + _task(id: 'google-parent', position: '1'), + ); + await database.tasksDao.upsertTask( + _task( + id: 'google-child', + position: '2', + parent: const Value('google-parent'), + ), + ); + await database.tasksDao.upsertTask( + _task( + id: 'nextcloud-parent', + position: '3', + icalUid: const Value('parent-uid'), + ), + ); + await database.tasksDao.upsertTask( + _task( + id: 'nextcloud-child', + position: '4', + parentUid: const Value('parent-uid'), + ), + ); + + final google = await repository + .watchTaskHierarchy('list-1', 'google-parent') + .first; + final nextcloud = await repository + .watchTaskHierarchy('list-1', 'nextcloud-child') + .first; + final tree = await repository + .watchTaskTree('list-1', const TaskViewFilter()) + .first; + + expect(google.subtasks.single.id, 'google-child'); + expect(nextcloud.parent?.id, 'nextcloud-parent'); + expect( + tree + .singleWhere((node) => node.task.id == 'google-parent') + .children + .single + .task + .id, + 'google-child', + ); + expect( + tree + .singleWhere((node) => node.task.id == 'nextcloud-parent') + .children + .single + .task + .id, + 'nextcloud-child', + ); + }); + + test( + 'Microsoft checklist subtask mutations keep an optimistic hierarchy', + () async { + var mutationCalls = 0; + repository = TasksRepository( + database: database, + accountId: 'account', + apiClient: _ChecklistTaskRemoteClient(), + onMutationQueued: () => mutationCalls += 1, + nowUtc: () => DateTime.utc(2026, 6, 4), + ); + await database.tasksDao.upsertTask(_task(id: 'parent', position: '1')); + + await repository.createSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + title: 'Checklist step', + ); + var task = (await database.tasksDao.listTasks( + 'account', + 'list-1', + )).single; + final created = decodeTaskChecklistItems( + task.microsoftChecklistItemsJson, + ).single; + var hierarchy = await repository + .watchTaskHierarchy('list-1', 'parent') + .first; + var operations = await database.pendingOpsDao.pendingOpsForReplay( + 'account', + DateTime.utc(2026, 6, 4, 1), + ); + + expect(created.title, 'Checklist step'); + expect(hierarchy.subtasks.single.kind, TaskSubtaskKind.checklistItem); + expect(operations.single.entityType, 'task_checklist_item'); + expect(operations.single.operation, 'create_task_checklist_item'); + + await repository.patchChecklistSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + checklistItemId: created.id, + completed: true, + ); + hierarchy = await repository.watchTaskHierarchy('list-1', 'parent').first; + expect(hierarchy.subtasks.single.completed, isTrue); + + await repository.deleteChecklistSubtask( + taskListId: 'list-1', + parentTaskId: 'parent', + checklistItemId: created.id, + ); + task = (await database.tasksDao.listTasks('account', 'list-1')).single; + operations = await database.pendingOpsDao.pendingOpsForReplay( + 'account', + DateTime.utc(2026, 6, 4, 1), + ); + expect( + decodeTaskChecklistItems(task.microsoftChecklistItemsJson), + isEmpty, + ); + expect(operations, isEmpty); + expect(mutationCalls, 3); + }, + ); } TasksRepository _repository( @@ -312,6 +481,10 @@ Future _insertAccount(AppDatabase database) { .insert( AccountsCompanion.insert( id: 'account', + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', authState: const Value('signed_in'), createdAtUtc: _now, updatedAtUtc: _now, @@ -337,6 +510,11 @@ TasksCompanion _task({ Value serverMissing = const Value.absent(), Value status = const Value.absent(), Value updatedUtc = const Value.absent(), + Value sortOrder = const Value.absent(), + Value parent = const Value.absent(), + Value parentUid = const Value.absent(), + Value icalUid = const Value.absent(), + Value microsoftChecklistItemsJson = const Value.absent(), String rawJson = '{}', }) { return TasksCompanion.insert( @@ -345,6 +523,11 @@ TasksCompanion _task({ id: id, title: id, position: Value(position), + sortOrder: sortOrder, + parent: parent, + parentUid: parentUid, + icalUid: icalUid, + microsoftChecklistItemsJson: microsoftChecklistItemsJson, hidden: hidden, serverMissing: serverMissing, status: status, @@ -356,3 +539,9 @@ TasksCompanion _task({ } const _now = '2026-06-04T00:00:00.000Z'; + +class _ChecklistTaskRemoteClient + implements TaskRemoteClient, TaskChecklistRemoteClient { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/features/tasks/presentation/task_details_draft_test.dart b/test/features/tasks/presentation/task_details_draft_test.dart index fbf9fab..11c03d9 100644 --- a/test/features/tasks/presentation/task_details_draft_test.dart +++ b/test/features/tasks/presentation/task_details_draft_test.dart @@ -1,6 +1,6 @@ import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; import 'package:busymax/src/features/tasks/presentation/task_details_draft.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -30,12 +30,59 @@ void main() { expect( draft.toPatch( task, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, localTimeZone: 'America/Vancouver', ), isEmpty, ); }); + + test('schedule reports a due date before the start date', () { + final draft = TaskDetailsDraft.fromTask( + _task(due: '2026-08-09', start: '2026-08-10'), + 'America/Vancouver', + ); + + expect(draft.scheduleIssue, TaskScheduleIssue.dueBeforeStart); + }); + + test('schedule accepts the same all-day start and due date', () { + final draft = TaskDetailsDraft.fromTask( + _task(due: '2026-08-10', start: '2026-08-10'), + 'America/Vancouver', + ); + + expect(draft.scheduleIssue, TaskScheduleIssue.none); + }); + + test('schedule reports mixed all-day and timed values', () { + final draft = TaskDetailsDraft.fromTask( + _task(due: '2026-08-10', start: '2026-08-10T09:00:00'), + 'America/Vancouver', + ); + + expect(draft.scheduleIssue, TaskScheduleIssue.mixedTimeModes); + }); +} + +TaskEntity _task({required String due, required String start}) { + return TaskEntity( + accountId: 'account', + taskListId: 'inbox', + id: 'task-1', + title: 'Task', + status: 'needsAction', + dueUtc: due.substring(0, 10), + microsoftDueDateTime: due, + microsoftDueTimeZone: 'America/Vancouver', + microsoftStartDateTime: start, + microsoftStartTimeZone: 'America/Vancouver', + localDirty: false, + pendingDelete: false, + pendingMove: false, + rawJson: '{}', + updatedLocalAtUtc: '2026-08-09T00:00:00.000Z', + ); } String _dateOnly(DateTime value) { diff --git a/test/features/tasks/presentation/task_details_pane_test.dart b/test/features/tasks/presentation/task_details_pane_test.dart index 3ac6a18..680b2dd 100644 --- a/test/features/tasks/presentation/task_details_pane_test.dart +++ b/test/features/tasks/presentation/task_details_pane_test.dart @@ -14,12 +14,16 @@ import 'package:busymax/src/core/time/time_zone_catalog.dart'; import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; import 'package:busymax/src/features/tasks/data/tasks_repository.dart'; +import 'package:busymax/src/features/tasks/presentation/ical_task_fields_editor.dart'; +import 'package:busymax/src/features/tasks/presentation/task_details_draft.dart'; import 'package:busymax/src/features/tasks/presentation/desktop_date_time_fields.dart'; import 'package:busymax/src/features/tasks/presentation/task_details_editor.dart'; import 'package:busymax/src/features/tasks/presentation/task_details_pane.dart'; import 'package:busymax/src/platform/native_dialog_service.dart'; import 'package:busymax/src/platform/native_menu_service.dart'; -import 'package:busymax/src/task_providers/task_provider.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/features/tasks/domain/task_capabilities.dart'; +import 'package:busymax/src/features/tasks/domain/task_checklist_item.dart'; import 'package:yaru/yaru.dart'; import '../../../test_localized_app.dart'; @@ -35,6 +39,13 @@ String _withVancouverTimeZone(String time) { return '$time ($_vancouverTimeZoneCode)'; } +String _testAuthority(BusyProvider provider) => switch (provider) { + BusyProvider.google => 'https://accounts.google.com', + BusyProvider.microsoft => 'https://login.microsoftonline.com/common', + BusyProvider.appleICloud => 'https://caldav.icloud.com', + BusyProvider.nextcloud => 'https://cloud.example.test', +}; + void main() { setUp(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -58,7 +69,7 @@ void main() { }); testWidgets('Task Details header shows Cancel and Save', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.text('Cancel'), findsOneWidget); expect(find.text('Edit Task'), findsOneWidget); @@ -73,10 +84,102 @@ void main() { ); }); + testWidgets( + 'task details renders parent, task children, and checklist steps', + (tester) async { + final parent = _switchTask('parent-task', 'Parent task'); + final child = _switchTask('child-task', 'Child task'); + final repository = _FakeTasksRepository( + hierarchy: TaskHierarchySnapshot( + parent: parent, + subtasks: [ + TaskSubtaskEntity.task(child, hasChildren: false), + TaskSubtaskEntity.checklistItem( + const TaskChecklistItemEntity( + id: 'step-1', + title: 'Checklist step', + completed: false, + rawJson: { + 'id': 'step-1', + 'displayName': 'Checklist step', + 'isChecked': false, + }, + ), + ), + ], + ), + ); + + await _pumpDetails( + tester, + microsoftTaskCollectionCapabilities, + repository: repository, + ); + + expect(find.text('Subtasks'), findsOneWidget); + expect(find.text('Parent task'), findsOneWidget); + expect(find.text('Child task'), findsOneWidget); + expect(find.text('Checklist step'), findsOneWidget); + expect( + find.byKey(const ValueKey('create-subtask-action')), + findsOneWidget, + ); + expect(find.text('Move to top'), findsNothing); + }, + ); + + testWidgets('read-only DAV task disables editing and deletion', ( + tester, + ) async { + await _pumpDetails( + tester, + nextcloudTaskCollectionCapabilities.asReadOnly(), + providerOverride: BusyProvider.nextcloud, + accountIdOverride: 'nextcloud:alex', + repository: _FakeTasksRepository(accountId: 'nextcloud:alex'), + ); + + final titleField = tester.widget(find.byType(TextField).first); + expect(titleField.enabled, isFalse); + expect(find.text('Delete Task'), findsNothing); + expect( + tester + .widget( + find.ancestor( + of: find.text('Save'), + matching: find.byType(ElevatedButton), + ), + ) + .onPressed, + isNull, + ); + }); + + testWidgets('recurring DAV task occurrence remains read-only', ( + tester, + ) async { + await _pumpDetails( + tester, + nextcloudTaskCollectionCapabilities, + providerOverride: BusyProvider.nextcloud, + accountIdOverride: 'nextcloud:alex', + repository: _FakeTasksRepository( + accountId: 'nextcloud:alex', + recurrenceIdKey: 'UTC:2026-06-06T14:30:00.000Z', + ), + ); + + expect( + tester.widget(find.byType(TextField).first).enabled, + isFalse, + ); + expect(find.text('Delete Task'), findsNothing); + }); + testWidgets('Cancel and Save use natural-width themed controls', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect( find.ancestor( @@ -121,7 +224,7 @@ void main() { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, theme: theme, modalEditorSurface: true, ); @@ -155,7 +258,7 @@ void main() { final colors = theme.extension()!; await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, theme: theme, modalEditorSurface: true, ); @@ -232,7 +335,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, theme: BusyMaxYaruTheme.build( brightness: Brightness.light, accentColor: const Color(0xFF3584E4), @@ -252,7 +355,7 @@ void main() { testWidgets('header places Cancel before centered title and Save after', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); final cancelLeft = tester.getTopLeft(find.text('Cancel')).dx; final titleCenter = tester.getCenter(find.text('Edit Task')).dx; @@ -268,7 +371,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -281,7 +384,7 @@ void main() { }); testWidgets('editing title enables Save', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); await tester.enterText(find.byType(TextField).first, 'Renamed task'); await tester.pump(); @@ -289,11 +392,36 @@ void main() { expect(_headerButtonOnPressed(tester, 'Save'), isNotNull); }); + testWidgets('Nextcloud due-before-start task explains and blocks Save', ( + tester, + ) async { + final repository = _FakeTasksRepository( + accountId: 'nextcloud:alex', + dueUtc: '2026-08-09', + microsoftDueDateTime: '2026-08-09', + microsoftStartDateTime: '2026-08-10', + ); + await _pumpDetails( + tester, + nextcloudTaskCollectionCapabilities, + providerOverride: BusyProvider.nextcloud, + accountIdOverride: 'nextcloud:alex', + repository: repository, + ); + + await tester.enterText(find.byType(TextField).first, 'Renamed task'); + await tester.pump(); + + expect(find.text('Due must not be before start.'), findsOneWidget); + expect(find.byKey(const ValueKey('task-schedule-error')), findsOneWidget); + expect(_headerButtonOnPressed(tester, 'Save'), isNull); + }); + testWidgets('Cancel discards draft without patching', (tester) async { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -311,7 +439,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -328,7 +456,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -350,7 +478,7 @@ void main() { var closed = false; await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, onClose: () => closed = true, ); @@ -394,7 +522,7 @@ void main() { var closed = false; await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, onClose: () { closed = true; @@ -416,7 +544,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -559,14 +687,60 @@ void main() { }); testWidgets('status controls are absent from Task Details', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.text('Open'), findsNothing); expect(find.text('Done'), findsNothing); }); + testWidgets( + 'shared public DAV task keeps status editable and classification locked', + (tester) async { + final task = TaskEntity( + accountId: 'nextcloud:n', + taskListId: 'list-1', + id: 'task-1', + title: 'Shared task', + status: 'needsAction', + providerStatus: 'NEEDS-ACTION', + taskClassification: 'PUBLIC', + localDirty: false, + pendingDelete: false, + pendingMove: false, + rawJson: '{}', + updatedLocalAtUtc: '2026-08-09T00:00:00.000Z', + ); + await tester.pumpWidget( + localizedTestApp( + child: Scaffold( + body: SingleChildScrollView( + child: IcalTaskFieldsEditor( + draft: TaskDetailsDraft.fromTask(task, 'UTC'), + capabilities: nextcloudTaskCollectionCapabilities + .withoutClassificationEditing(), + enabled: true, + onChanged: (_) {}, + ), + ), + ), + ), + ); + + BusyMaxComboRow combo(String title) => + tester.widget>( + find.byWidgetPredicate( + (widget) => + widget is BusyMaxComboRow && widget.title == title, + ), + ); + + expect(combo('Status').enabled, isTrue); + expect(combo('Classification').enabled, isFalse); + }, + ); + testWidgets('no visible timezone helper text or UTC appears', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.textContaining('Time zone:'), findsNothing); expect(find.text('UTC'), findsNothing); @@ -576,7 +750,7 @@ void main() { testWidgets('Task Details uses BusyMax grouped rows without section blocks', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.byType(Dialog), findsNothing); expect(find.byType(TaskDetailsPane), findsOneWidget); @@ -592,7 +766,7 @@ void main() { testWidgets('Task Details section labels use shared section typography', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); final dueText = tester.widget(find.text('Due')); final context = tester.element(find.text('Due')); @@ -616,7 +790,7 @@ void main() { var closed = false; await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: _FakeTasksRepository(missingTask: true), onClose: () { closed = true; @@ -664,7 +838,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, alwaysUse24HourFormat: true, ); @@ -684,7 +858,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, alwaysUse24HourFormat: false, ); @@ -703,7 +877,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, locale: const Locale('de'), alwaysUse24HourFormat: true, ); @@ -718,7 +892,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, locale: const Locale('fr'), alwaysUse24HourFormat: true, ); @@ -733,7 +907,7 @@ void main() { ) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, locale: const Locale('es'), alwaysUse24HourFormat: true, ); @@ -767,7 +941,7 @@ void main() { testWidgets('Google keeps unsupported fields out of main editor by default', ( tester, ) async { - await _pumpDetails(tester, googleTaskProviderCapabilities); + await _pumpDetails(tester, googleTaskCollectionCapabilities); expect(find.text('Due'), findsOneWidget); expect(_dateRowFinder('Due date'), findsOneWidget); @@ -791,7 +965,7 @@ void main() { ) async { await _pumpDetails( tester, - googleTaskProviderCapabilities, + googleTaskCollectionCapabilities, accountIdOverride: 'google-generated-local-id', includeAccountIdentity: false, ); @@ -809,7 +983,7 @@ void main() { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, accountIdOverride: accountId, accountsStream: accounts.stream, ); @@ -819,7 +993,9 @@ void main() { accounts.add([ const AccountEntity( id: accountId, - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: accountId, authState: 'signed_in', displayName: 'Microsoft User', email: 'microsoft@example.com', @@ -851,7 +1027,7 @@ void main() { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, accountIdOverride: accountId, accountsStream: accounts.stream, onClose: () => closeCalls += 1, @@ -861,7 +1037,9 @@ void main() { accounts.add([ const AccountEntity( id: accountId, - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: accountId, authState: 'signed_in', displayName: 'Microsoft User', email: 'microsoft@example.com', @@ -885,7 +1063,7 @@ void main() { testWidgets('unsupported provider text is not rendered for Google', ( tester, ) async { - await _pumpDetails(tester, googleTaskProviderCapabilities); + await _pumpDetails(tester, googleTaskCollectionCapabilities); expect(find.text('Provider features'), findsNothing); expect(find.text('Not supported by Google Tasks.'), findsNothing); @@ -899,7 +1077,7 @@ void main() { testWidgets( 'Microsoft shows Due, Start, Reminder, Repeat, and Organization', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.text('Due'), findsOneWidget); expect(find.text('Start'), findsOneWidget); @@ -916,7 +1094,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -955,7 +1133,7 @@ void main() { ); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -985,7 +1163,7 @@ void main() { }); testWidgets('Due group appears before separate Start group', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); final dueTop = tester.getTopLeft(find.text('Due')).dy; final startTop = tester.getTopLeft(find.text('Start')).dy; @@ -1000,7 +1178,7 @@ void main() { testWidgets('Reminder absent state shows centered Add Reminder', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.text('Add Reminder'), findsOneWidget); expect( @@ -1016,7 +1194,7 @@ void main() { testWidgets('reminder uses date and time rows when enabled', (tester) async { await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, reminderOn: true, alwaysUse24HourFormat: true, ); @@ -1035,7 +1213,7 @@ void main() { }); testWidgets('delete action is separated and destructive', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); await tester.scrollUntilVisible( find.text('Delete Task'), @@ -1059,7 +1237,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1081,7 +1259,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1095,7 +1273,7 @@ void main() { }); testWidgets('metadata is not shown in Edit Task', (tester) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect(find.text('Metadata'), findsNothing); expect(find.text('task-1'), findsNothing); @@ -1105,7 +1283,7 @@ void main() { testWidgets('list move unsupported explanation is not rendered', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); expect( find.text( @@ -1141,7 +1319,7 @@ void main() { calls.add(call); return null; }); - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); await _openDatePicker(tester, 'Due date'); @@ -1232,7 +1410,7 @@ void main() { }); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, alwaysUse24HourFormat: false, ); @@ -1254,7 +1432,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1292,7 +1470,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1325,7 +1503,7 @@ void main() { ); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1363,7 +1541,7 @@ void main() { ); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, repository: repository, ); @@ -1382,7 +1560,7 @@ void main() { testWidgets('populated time field renders its floating label and value', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); final field = _labeledTextFormFieldFinder('Due time'); final label = find.text('Due time'); @@ -1479,7 +1657,7 @@ void main() { final repository = _FakeTasksRepository(); await _pumpDetails( tester, - microsoftTaskProviderCapabilities, + microsoftTaskCollectionCapabilities, alwaysUse24HourFormat: true, repository: repository, ); @@ -1510,7 +1688,7 @@ void main() { testWidgets('date field does not render a Flutter calendar grid', ( tester, ) async { - await _pumpDetails(tester, microsoftTaskProviderCapabilities); + await _pumpDetails(tester, microsoftTaskCollectionCapabilities); await _openDatePicker(tester, 'Due date'); @@ -1529,7 +1707,7 @@ Finder _confirmDialogButton(String label) { Future _pumpDetails( WidgetTester tester, - TaskProviderCapabilities capabilities, { + TaskCollectionCapabilities capabilities, { Locale locale = const Locale('en'), bool? alwaysUse24HourFormat, bool reminderOn = false, @@ -1542,13 +1720,16 @@ Future _pumpDetails( Stream>? accountsStream, ThemeData? theme, bool modalEditorSurface = false, + BusyProvider? providerOverride, }) async { final accountId = accountIdOverride ?? (capabilities.supportsDueTime ? 'microsoft:m' : 'google:g'); - final provider = capabilities.supportsDueTime - ? TaskProvider.microsoft - : TaskProvider.google; + final provider = + providerOverride ?? + (capabilities.supportsDueTime + ? BusyProvider.microsoft + : BusyProvider.google); final accountDisplayName = includeAccountIdentity ? displayName ?? (capabilities.supportsDueTime ? 'Microsoft User' : 'Google User') @@ -1571,12 +1752,20 @@ Future _pumpDetails( AccountEntity( id: accountId, provider: provider, + authority: _testAuthority(provider), + providerAccountId: accountId, authState: 'signed_in', displayName: accountDisplayName, email: accountEmail, ), ), selectedAccountCapabilitiesProvider.overrideWithValue(capabilities), + davTaskCollectionCapabilitiesProvider.overrideWith( + (ref, key) async => capabilities, + ), + davCollectionsStreamProvider.overrideWith( + (ref) => Stream.value(const []), + ), localTimeZoneProvider.overrideWithValue('UTC'), accountsStreamProvider.overrideWith((ref) { return accountsStream ?? @@ -1584,6 +1773,8 @@ Future _pumpDetails( AccountEntity( id: accountId, provider: provider, + authority: _testAuthority(provider), + providerAccountId: accountId, authState: 'signed_in', displayName: accountDisplayName, email: accountEmail, @@ -1647,11 +1838,16 @@ Future _pumpSwitchingDetails( ProviderScope( overrides: [ localTimeZoneProvider.overrideWithValue('UTC'), + davCollectionsStreamProvider.overrideWith( + (ref) => Stream.value(const []), + ), accountsStreamProvider.overrideWith((ref) { return Stream.value([ const AccountEntity( id: 'microsoft:m', - provider: TaskProvider.microsoft, + provider: BusyProvider.microsoft, + authority: 'https://login.microsoftonline.com/common', + providerAccountId: 'm', authState: 'signed_in', displayName: 'Microsoft User', email: 'microsoft@example.com', @@ -1820,17 +2016,23 @@ class _FakeTasksRepository implements TasksRepository { this.accountId = 'microsoft:m', this.reminderOn = false, this.missingTask = false, + this.dueUtc = '2026-06-06', this.microsoftDueDateTime = '2026-06-06T14:30:00', this.microsoftStartDateTime = '2026-06-04T07:00:00.0000000', this.categorySuggestions = const [], + this.recurrenceIdKey, + this.hierarchy = const TaskHierarchySnapshot(parent: null, subtasks: []), }); final String accountId; final bool reminderOn; final bool missingTask; + final String dueUtc; final String microsoftDueDateTime; final String? microsoftStartDateTime; final List categorySuggestions; + final String? recurrenceIdKey; + final TaskHierarchySnapshot hierarchy; final List patches = []; final List moves = []; var deleteCalls = 0; @@ -1851,8 +2053,9 @@ class _FakeTasksRepository implements TasksRepository { pendingMove: false, rawJson: '{}', updatedLocalAtUtc: '2026-06-04T00:00:00.000Z', + recurrenceIdKey: recurrenceIdKey, status: 'needsAction', - dueUtc: '2026-06-06', + dueUtc: dueUtc, microsoftDueDateTime: microsoftDueDateTime, microsoftDueTimeZone: 'America/Vancouver', microsoftStartDateTime: microsoftStartDateTime, @@ -1871,6 +2074,14 @@ class _FakeTasksRepository implements TasksRepository { return Stream.value(categorySuggestions); } + @override + Stream watchTaskHierarchy( + String taskListId, + String taskId, + ) { + return Stream.value(hierarchy); + } + @override Future patchTask( String taskListId, @@ -1922,6 +2133,16 @@ class _SwitchingTasksRepository implements TasksRepository { .stream; } + @override + Stream watchTaskHierarchy( + String taskListId, + String taskId, + ) { + return Stream.value( + const TaskHierarchySnapshot(parent: null, subtasks: []), + ); + } + @override Stream> watchCategorySuggestions() { return Stream.value(const []); diff --git a/test/fixtures/schema_v5_production_like.sql b/test/fixtures/schema_v5_production_like.sql new file mode 100644 index 0000000..acd1ca9 --- /dev/null +++ b/test/fixtures/schema_v5_production_like.sql @@ -0,0 +1,48 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE IF NOT EXISTS "accounts" ("id" TEXT NOT NULL, "provider" TEXT NOT NULL DEFAULT 'google', "provider_account_id" TEXT NULL, "display_name" TEXT NULL, "email" TEXT NULL, "tenant_id" TEXT NULL, "account_avatar_url" TEXT NULL, "provider_metadata_json" TEXT NULL, "auth_state" TEXT NOT NULL DEFAULT 'signed_out', "calendars_enabled" INTEGER NOT NULL DEFAULT 1 CHECK ("calendars_enabled" IN (0, 1)), "tasks_enabled" INTEGER NOT NULL DEFAULT 1 CHECK ("tasks_enabled" IN (0, 1)), "granted_scopes" TEXT NOT NULL DEFAULT '', "created_at_utc" TEXT NOT NULL, "updated_at_utc" TEXT NOT NULL, "last_successful_sync_at_utc" TEXT NULL, "last_full_sync_at_utc" TEXT NULL, PRIMARY KEY ("id")); +INSERT INTO accounts VALUES('google:g-sub','google','g-sub','Google User','g@example.com',NULL,NULL,NULL,'signed_in',1,1,'calendar tasks','2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z',NULL,NULL); +INSERT INTO accounts VALUES('microsoft:m-sub','microsoft','m-sub','Microsoft User','m@example.com','TENANT-A',NULL,NULL,'signed_in',1,1,'calendar tasks','2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z',NULL,NULL); +CREATE TABLE IF NOT EXISTS "task_lists" ("account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "id" TEXT NOT NULL, "kind" TEXT NULL, "etag" TEXT NULL, "title" TEXT NOT NULL, "updated_utc" TEXT NULL, "self_link" TEXT NULL, "raw_json" TEXT NOT NULL, "provider_list_kind" TEXT NULL, "is_owner" INTEGER NULL CHECK ("is_owner" IN (0, 1)), "is_shared" INTEGER NULL CHECK ("is_shared" IN (0, 1)), "delta_link" TEXT NULL, "provider_metadata_json" TEXT NULL, "server_missing" INTEGER NOT NULL DEFAULT 0 CHECK ("server_missing" IN (0, 1)), "local_dirty" INTEGER NOT NULL DEFAULT 0 CHECK ("local_dirty" IN (0, 1)), "pending_delete" INTEGER NOT NULL DEFAULT 0 CHECK ("pending_delete" IN (0, 1)), "last_synced_at_utc" TEXT NULL, "created_local_at_utc" TEXT NOT NULL, "updated_local_at_utc" TEXT NOT NULL, PRIMARY KEY ("account_id", "id")); +INSERT INTO task_lists VALUES('google:g-sub','g-list',NULL,NULL,'Google Tasks',NULL,NULL,'{"provider":"google"}',NULL,NULL,NULL,NULL,NULL,0,0,0,NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +INSERT INTO task_lists VALUES('microsoft:m-sub','m-list',NULL,NULL,'Microsoft Tasks',NULL,NULL,'{"provider":"microsoft"}','defaultList',NULL,NULL,'https://graph.example/delta',NULL,0,0,0,NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +CREATE TABLE IF NOT EXISTS "tasks" ("account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "task_list_id" TEXT NOT NULL, "id" TEXT NOT NULL, "kind" TEXT NULL, "etag" TEXT NULL, "title" TEXT NOT NULL, "updated_utc" TEXT NULL, "self_link" TEXT NULL, "parent" TEXT NULL, "position" TEXT NULL, "notes" TEXT NULL, "status" TEXT NULL, "due_utc" TEXT NULL, "completed_utc" TEXT NULL, "provider_status" TEXT NULL, "body_content" TEXT NULL, "body_content_type" TEXT NULL, "microsoft_due_date_time" TEXT NULL, "microsoft_due_time_zone" TEXT NULL, "microsoft_start_date_time" TEXT NULL, "microsoft_start_time_zone" TEXT NULL, "microsoft_reminder_date_time" TEXT NULL, "microsoft_reminder_time_zone" TEXT NULL, "microsoft_is_reminder_on" INTEGER NULL CHECK ("microsoft_is_reminder_on" IN (0, 1)), "microsoft_completed_date_time" TEXT NULL, "microsoft_completed_time_zone" TEXT NULL, "recurrence_json" TEXT NULL, "importance" TEXT NULL, "categories_json" TEXT NULL, "has_attachments" INTEGER NULL CHECK ("has_attachments" IN (0, 1)), "provider_metadata_json" TEXT NULL, "deleted" INTEGER NULL CHECK ("deleted" IN (0, 1)), "hidden" INTEGER NULL CHECK ("hidden" IN (0, 1)), "links_json" TEXT NULL, "web_view_link" TEXT NULL, "assignment_info_json" TEXT NULL, "raw_json" TEXT NOT NULL, "server_missing" INTEGER NOT NULL DEFAULT 0 CHECK ("server_missing" IN (0, 1)), "local_dirty" INTEGER NOT NULL DEFAULT 0 CHECK ("local_dirty" IN (0, 1)), "pending_delete" INTEGER NOT NULL DEFAULT 0 CHECK ("pending_delete" IN (0, 1)), "pending_move" INTEGER NOT NULL DEFAULT 0 CHECK ("pending_move" IN (0, 1)), "local_created" INTEGER NOT NULL DEFAULT 0 CHECK ("local_created" IN (0, 1)), "sync_base_updated_utc" TEXT NULL, "last_synced_at_utc" TEXT NULL, "created_local_at_utc" TEXT NOT NULL, "updated_local_at_utc" TEXT NOT NULL, PRIMARY KEY ("account_id", "task_list_id", "id"), FOREIGN KEY(account_id, task_list_id) REFERENCES task_lists(account_id, id) ON DELETE CASCADE); +INSERT INTO tasks VALUES('google:g-sub','g-list','g-parent',NULL,NULL,'Parent',NULL,NULL,NULL,'1',NULL,'needsAction',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'{"id":"g-parent"}',0,0,0,0,0,NULL,NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +INSERT INTO tasks VALUES('google:g-sub','g-list','g-child',NULL,NULL,'Child',NULL,NULL,'g-parent','2',NULL,'needsAction',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'{"id":"g-child"}',0,0,0,0,0,NULL,NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +INSERT INTO tasks VALUES('microsoft:m-sub','m-list','m-recurring',NULL,NULL,'Recurring task',NULL,NULL,NULL,'1',NULL,'notStarted',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'{"pattern":{"type":"weekly"}}',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'{"id":"m-recurring"}',0,0,0,0,0,NULL,NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +CREATE TABLE IF NOT EXISTS "pending_ops" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "provider" TEXT NULL, "entity_type" TEXT NOT NULL, "operation" TEXT NOT NULL, "operation_type" TEXT NULL, "task_list_id" TEXT NULL, "task_id" TEXT NULL, "calendar_source_id" TEXT NULL, "provider_calendar_id" TEXT NULL, "event_id" TEXT NULL, "local_temp_id" TEXT NULL, "depends_on_op_id" TEXT NULL, "request_json" TEXT NOT NULL, "baseline_updated_utc" TEXT NULL, "baseline_raw_json" TEXT NULL, "attempt_count" INTEGER NOT NULL DEFAULT 0, "next_attempt_at_utc" TEXT NULL, "last_error_code" TEXT NULL, "last_error_message" TEXT NULL, "state" TEXT NOT NULL DEFAULT 'pending', "last_error" TEXT NULL, "created_at_utc" TEXT NOT NULL, "updated_at_utc" TEXT NOT NULL, PRIMARY KEY ("id")); +INSERT INTO pending_ops VALUES('op-create','google:g-sub','google','task','create_task','create','g-list','local-task',NULL,NULL,NULL,NULL,NULL,'{"title":"new"}',NULL,NULL,0,NULL,NULL,NULL,'pending',NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +INSERT INTO pending_ops VALUES('op-update','microsoft:m-sub','microsoft','task','patch_task','update','m-list','m-recurring',NULL,NULL,NULL,NULL,NULL,'{"title":"changed"}',NULL,'{"title":"Recurring task"}',0,NULL,NULL,NULL,'pending',NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +INSERT INTO pending_ops VALUES('op-delete','google:g-sub','google','task','delete_task','delete','g-list','g-child',NULL,NULL,NULL,NULL,NULL,'{}',NULL,'{"id":"g-child"}',0,NULL,NULL,NULL,'pending',NULL,'2026-08-01T00:00:00.000Z','2026-08-01T00:00:00.000Z'); +CREATE TABLE IF NOT EXISTS "sync_runs" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "provider" TEXT NULL, "mode" TEXT NOT NULL, "started_at_utc" TEXT NOT NULL, "finished_at_utc" TEXT NULL, "status" TEXT NOT NULL, "task_lists_seen" INTEGER NOT NULL DEFAULT 0, "tasks_seen" INTEGER NOT NULL DEFAULT 0, "pending_ops_applied" INTEGER NOT NULL DEFAULT 0, "error_code" TEXT NULL, "error_message" TEXT NULL, PRIMARY KEY ("id")); +CREATE TABLE IF NOT EXISTS "calendar_sources" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "provider" TEXT NOT NULL, "provider_calendar_id" TEXT NOT NULL, "summary" TEXT NOT NULL, "description" TEXT NULL, "primary_calendar" INTEGER NOT NULL DEFAULT 0 CHECK ("primary_calendar" IN (0, 1)), "selected" INTEGER NOT NULL DEFAULT 1 CHECK ("selected" IN (0, 1)), "hidden" INTEGER NOT NULL DEFAULT 0 CHECK ("hidden" IN (0, 1)), "read_only" INTEGER NOT NULL DEFAULT 0 CHECK ("read_only" IN (0, 1)), "background_color" TEXT NULL, "foreground_color" TEXT NULL, "color_id" TEXT NULL, "time_zone" TEXT NULL, "access_role" TEXT NULL, "is_deleted" INTEGER NOT NULL DEFAULT 0 CHECK ("is_deleted" IN (0, 1)), "raw_json" TEXT NULL, "created_at_local" INTEGER NOT NULL, "updated_at_local" INTEGER NOT NULL, PRIMARY KEY ("id")); +INSERT INTO calendar_sources VALUES('g-source','google:g-sub','google','g-calendar','Google Calendar',NULL,0,1,0,0,NULL,NULL,NULL,NULL,NULL,0,NULL,1785542400000,1785542400000); +INSERT INTO calendar_sources VALUES('m-source','microsoft:m-sub','microsoft','m-calendar','Microsoft Calendar',NULL,0,1,0,0,NULL,NULL,NULL,NULL,NULL,0,NULL,1785542400000,1785542400000); +CREATE TABLE IF NOT EXISTS "calendar_events" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "calendar_source_id" TEXT NOT NULL REFERENCES calendar_sources (id) ON DELETE CASCADE, "provider" TEXT NOT NULL, "provider_calendar_id" TEXT NOT NULL, "provider_event_id" TEXT NOT NULL, "provider_recurring_event_id" TEXT NULL, "provider_original_start_key" TEXT NULL, "etag_or_change_key" TEXT NULL, "status" TEXT NULL, "title" TEXT NOT NULL, "description" TEXT NULL, "location" TEXT NULL, "all_day" INTEGER NOT NULL DEFAULT 0 CHECK ("all_day" IN (0, 1)), "start_date" TEXT NULL, "start_date_time" TEXT NULL, "start_time_zone" TEXT NULL, "end_date" TEXT NULL, "end_date_time" TEXT NULL, "end_time_zone" TEXT NULL, "recurrence_json" TEXT NULL, "reminders_json" TEXT NULL, "attendees_json" TEXT NULL, "categories_json" TEXT NULL, "organizer_json" TEXT NULL, "creator_json" TEXT NULL, "color_id" TEXT NULL, "color_hex" TEXT NULL, "visibility" TEXT NULL, "transparency_or_show_as" TEXT NULL, "event_type" TEXT NULL, "web_link" TEXT NULL, "conference_json" TEXT NULL, "attachments_json" TEXT NULL, "is_cancelled" INTEGER NOT NULL DEFAULT 0 CHECK ("is_cancelled" IN (0, 1)), "is_deleted" INTEGER NOT NULL DEFAULT 0 CHECK ("is_deleted" IN (0, 1)), "raw_json" TEXT NULL, "created_at_server" TEXT NULL, "updated_at_server" TEXT NULL, "created_at_local" INTEGER NOT NULL, "updated_at_local" INTEGER NOT NULL, "sync_status" TEXT NOT NULL DEFAULT 'synced', "baseline_raw_json" TEXT NULL, PRIMARY KEY ("id")); +INSERT INTO calendar_events VALUES('g-event','google:g-sub','g-source','google','g-calendar','g-event-remote',NULL,NULL,NULL,NULL,'Recurring event',NULL,NULL,0,NULL,'2026-08-01T10:00:00Z',NULL,NULL,'2026-08-01T11:00:00Z',NULL,'["RRULE:FREQ=WEEKLY"]',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,0,'{"recurrence":["RRULE:FREQ=WEEKLY"]}',NULL,NULL,1785542400000,1785542400000,'synced','{"recurrence":["RRULE:FREQ=WEEKLY"]}'); +INSERT INTO calendar_events VALUES('m-event','microsoft:m-sub','m-source','microsoft','m-calendar','m-event-remote',NULL,NULL,NULL,NULL,'Microsoft event',NULL,NULL,0,NULL,'2026-08-02T10:00:00Z',NULL,NULL,'2026-08-02T11:00:00Z',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,0,'{"id":"m-event-remote"}',NULL,NULL,1785542400000,1785542400000,'synced','{"id":"m-event-remote"}'); +CREATE TABLE IF NOT EXISTS "calendar_event_attendees" ("id" TEXT NOT NULL, "calendar_event_id" TEXT NOT NULL REFERENCES calendar_events (id) ON DELETE CASCADE, "email" TEXT NOT NULL, "display_name" TEXT NULL, "response_status" TEXT NULL, "optional" INTEGER NOT NULL DEFAULT 0 CHECK ("optional" IN (0, 1)), "organizer" INTEGER NOT NULL DEFAULT 0 CHECK ("organizer" IN (0, 1)), "self" INTEGER NOT NULL DEFAULT 0 CHECK ("self" IN (0, 1)), "raw_json" TEXT NULL, PRIMARY KEY ("id")); +CREATE TABLE IF NOT EXISTS "calendar_event_reminders" ("id" TEXT NOT NULL, "calendar_event_id" TEXT NOT NULL REFERENCES calendar_events (id) ON DELETE CASCADE, "provider" TEXT NOT NULL, "method" TEXT NULL, "minutes_before" INTEGER NULL, "absolute_time" TEXT NULL, "enabled" INTEGER NOT NULL DEFAULT 1 CHECK ("enabled" IN (0, 1)), "raw_json" TEXT NULL, PRIMARY KEY ("id")); +CREATE TABLE IF NOT EXISTS "calendar_sync_states" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "calendar_source_id" TEXT NULL REFERENCES calendar_sources (id) ON DELETE CASCADE, "provider" TEXT NOT NULL, "sync_kind" TEXT NOT NULL, "range_start" TEXT NULL, "range_end" TEXT NULL, "google_sync_token" TEXT NULL, "microsoft_delta_link" TEXT NULL, "last_full_sync_at" INTEGER NULL, "last_incremental_sync_at" INTEGER NULL, "last_error" TEXT NULL, "raw_state_json" TEXT NULL, PRIMARY KEY ("id")); +INSERT INTO calendar_sync_states VALUES('g-cursor','google:g-sub','g-source','google','events','2025-08-01','2028-09-01','g-token',NULL,1785542400000,1785542500000,NULL,'{"expanded":true}'); +INSERT INTO calendar_sync_states VALUES('m-cursor','microsoft:m-sub','m-source','microsoft','events','2025-08-01','2028-09-01',NULL,'https://graph.example/delta-2',1785542400000,1785542600000,NULL,NULL); +CREATE TABLE IF NOT EXISTS "calendar_colors" ("provider" TEXT NOT NULL, "color_type" TEXT NOT NULL, "color_id" TEXT NOT NULL, "background" TEXT NOT NULL, "foreground" TEXT NULL, "raw_json" TEXT NULL, PRIMARY KEY ("provider", "color_type", "color_id")); +CREATE TABLE IF NOT EXISTS "schedule_item_overrides" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "source_type" TEXT NOT NULL, "source_id" TEXT NOT NULL, "override_json" TEXT NOT NULL, "created_at_local" INTEGER NOT NULL, "updated_at_local" INTEGER NOT NULL, PRIMARY KEY ("id")); +CREATE TABLE IF NOT EXISTS "notification_schedule" ("id" TEXT NOT NULL, "account_id" TEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE, "source_type" TEXT NOT NULL, "source_id" TEXT NOT NULL, "scheduled_at_utc" INTEGER NOT NULL, "title" TEXT NOT NULL, "body" TEXT NULL, "sent_at_utc" INTEGER NULL, "dismissed_at_utc" INTEGER NULL, "snoozed_until_utc" INTEGER NULL, "created_at_local" INTEGER NOT NULL, "updated_at_local" INTEGER NOT NULL, PRIMARY KEY ("id")); +CREATE INDEX idx_accounts_provider ON accounts(provider); +CREATE UNIQUE INDEX idx_accounts_provider_account ON accounts(provider, provider_account_id) WHERE provider_account_id IS NOT NULL; +CREATE INDEX idx_task_lists_account_title ON task_lists(account_id, title COLLATE NOCASE); +CREATE INDEX idx_task_lists_dirty ON task_lists(account_id, local_dirty, pending_delete); +CREATE INDEX idx_tasks_list_order ON tasks(account_id, task_list_id, parent, position); +CREATE INDEX idx_tasks_status_due ON tasks(account_id, task_list_id, status, due_utc); +CREATE INDEX idx_tasks_dirty ON tasks(account_id, local_dirty, pending_delete, pending_move); +CREATE INDEX idx_tasks_updated ON tasks(account_id, task_list_id, updated_utc); +CREATE UNIQUE INDEX idx_calendar_sources_provider_id ON calendar_sources(account_id, provider, provider_calendar_id); +CREATE INDEX idx_calendar_sources_visible ON calendar_sources(account_id, selected, hidden, is_deleted); +CREATE UNIQUE INDEX idx_calendar_events_provider_id ON calendar_events(account_id, provider, provider_calendar_id, provider_event_id, provider_original_start_key); +CREATE INDEX idx_calendar_events_range ON calendar_events(account_id, calendar_source_id, all_day, start_date, start_date_time, end_date, end_date_time); +CREATE INDEX idx_calendar_events_dirty ON calendar_events(account_id, sync_status, is_deleted); +CREATE UNIQUE INDEX idx_calendar_sync_states_scope ON calendar_sync_states(account_id, provider, sync_kind, calendar_source_id, range_start, range_end); +CREATE INDEX idx_notification_schedule_due ON notification_schedule(scheduled_at_utc, sent_at_utc, dismissed_at_utc, snoozed_until_utc); +PRAGMA user_version=5; +COMMIT; diff --git a/test/google_calendar/google_calendar_api_client_test.dart b/test/google_calendar/google_calendar_api_client_test.dart index 486ec86..b42ce43 100644 --- a/test/google_calendar/google_calendar_api_client_test.dart +++ b/test/google_calendar/google_calendar_api_client_test.dart @@ -67,7 +67,10 @@ void main() { .insert( AccountsCompanion.insert( id: 'account', - provider: const Value('google'), + provider: 'google', + authority: 'https://accounts.google.com', + providerAccountId: 'google-account', + credentialKind: 'oauth', authState: const Value('signed_in'), grantedScopes: const Value(''), createdAtUtc: '2026-07-01T00:00:00.000Z', diff --git a/test/google_tasks/api/tasklists_patch_test.dart b/test/google_tasks/api/tasklists_patch_test.dart index 07e0c47..e5c6c27 100644 --- a/test/google_tasks/api/tasklists_patch_test.dart +++ b/test/google_tasks/api/tasklists_patch_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'test_api_client_support.dart'; diff --git a/test/google_tasks/api/tasklists_update_test.dart b/test/google_tasks/api/tasklists_update_test.dart index 62620f5..e447710 100644 --- a/test/google_tasks/api/tasklists_update_test.dart +++ b/test/google_tasks/api/tasklists_update_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'test_api_client_support.dart'; diff --git a/test/google_tasks/api/tasks_insert_test.dart b/test/google_tasks/api/tasks_insert_test.dart index a80143a..df6b13b 100644 --- a/test/google_tasks/api/tasks_insert_test.dart +++ b/test/google_tasks/api/tasks_insert_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'test_api_client_support.dart'; diff --git a/test/google_tasks/api/tasks_patch_test.dart b/test/google_tasks/api/tasks_patch_test.dart index f3774d0..c2cb624 100644 --- a/test/google_tasks/api/tasks_patch_test.dart +++ b/test/google_tasks/api/tasks_patch_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'test_api_client_support.dart'; diff --git a/test/google_tasks/api/tasks_update_test.dart b/test/google_tasks/api/tasks_update_test.dart index 8f3d4c5..0d4de44 100644 --- a/test/google_tasks/api/tasks_update_test.dart +++ b/test/google_tasks/api/tasks_update_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'test_api_client_support.dart'; diff --git a/test/google_tasks/http/authenticated_http_client_test.dart b/test/google_tasks/http/authenticated_http_client_test.dart index 0fad2fa..63c2783 100644 --- a/test/google_tasks/http/authenticated_http_client_test.dart +++ b/test/google_tasks/http/authenticated_http_client_test.dart @@ -6,15 +6,20 @@ import 'package:http/testing.dart'; import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/google_tasks/http/authenticated_http_client.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_service.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; void main() { test('attaches bearer authorization header', () async { late String? authorization; - final store = InMemoryOAuthTokenStore(); - await store.saveTokenSet('account', _tokenSet('access')); + final store = InMemorySecretStore(); + await store.saveOAuthTokenSet( + 'account', + BusyProvider.google, + _tokenSet('access'), + ); await store.setActiveAccountId('account'); final client = AuthenticatedHttpClient( @@ -37,8 +42,12 @@ void main() { }); test('refreshes once after 401 and retries original request', () async { - final store = InMemoryOAuthTokenStore(); - await store.saveTokenSet('account', _tokenSet('old-access')); + final store = InMemorySecretStore(); + await store.saveOAuthTokenSet( + 'account', + BusyProvider.google, + _tokenSet('old-access'), + ); await store.setActiveAccountId('account'); var apiCalls = 0; @@ -78,7 +87,7 @@ void main() { }); } -OAuthService _service(OAuthTokenStore store, http.Client tokenClient) { +OAuthService _service(SecretStore store, http.Client tokenClient) { return OAuthService( config: const BuildConfig( googleOAuthClientId: 'client-id', diff --git a/test/google_tasks/oauth/callback_test.dart b/test/google_tasks/oauth/callback_test.dart index bc2585e..291efd8 100644 --- a/test/google_tasks/oauth/callback_test.dart +++ b/test/google_tasks/oauth/callback_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; void main() { test('callback parser accepts code and validates state', () { diff --git a/test/google_tasks/oauth/loopback_flow_test.dart b/test/google_tasks/oauth/loopback_flow_test.dart index 431943e..9859185 100644 --- a/test/google_tasks/oauth/loopback_flow_test.dart +++ b/test/google_tasks/oauth/loopback_flow_test.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; void main() { test('cancelSignIn is idempotent after server already closed', () async { diff --git a/test/google_tasks/oauth/oauth_token_store_test.dart b/test/google_tasks/oauth/oauth_token_store_test.dart index 775db47..d903b6b 100644 --- a/test/google_tasks/oauth/oauth_token_store_test.dart +++ b/test/google_tasks/oauth/oauth_token_store_test.dart @@ -1,11 +1,14 @@ import 'dart:io'; +import 'dart:convert'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; -import 'package:busymax/src/google_tasks/oauth/portal_encrypted_oauth_token_store.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/core/secrets/portal_encrypted_secret_store.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:posix/posix.dart' show chmod; void main() { late Directory tempDir; @@ -23,9 +26,9 @@ void main() { }); test( - 'secure storage platform failures become OAuth storage errors', + 'secure storage platform failures become typed secret-store errors', () async { - final store = SecureOAuthTokenStore( + final store = SecureSecretStore( _ThrowingSecureStorage( PlatformException( code: 'KeyringLocked', @@ -37,16 +40,12 @@ void main() { await expectLater( store.readActiveAccountId(), throwsA( - isA() - .having( - (error) => error.code, - 'code', - 'OAuthSecureStorageUnavailable', - ) + isA() + .having((error) => error.code, 'code', 'SecretStoreUnavailable') .having( (error) => error.message, 'message', - secureTokenStorageUnavailableMessage, + secretStorageUnavailableMessage, ), ), ); @@ -54,19 +53,111 @@ void main() { ); test( - 'portal encrypted token store does not write plaintext tokens', + 'credential records are versioned, typed, and redacted in diagnostics', + () { + final records = [ + OAuthSecretRecord(provider: BusyProvider.google, tokenSet: _tokenSet()), + AppleICloudSecretRecord( + username: 'User@Example.com', + appSpecificPassword: 'abcd-efgh-ijkl-mnop', + ), + NextcloudSecretRecord( + canonicalServer: Uri.parse('https://cloud.example.test/nextcloud'), + loginName: 'alex', + appPassword: 'nextcloud-app-secret', + ), + ]; + + for (final record in records) { + final json = record.toJson(); + expect(json['schemaVersion'], secretRecordSchemaVersion); + final restored = SecretRecord.fromJson( + (jsonDecode(jsonEncode(json)) as Map).cast(), + ); + expect(restored.runtimeType, record.runtimeType); + expect(restored.provider, record.provider); + expect(restored.kind, record.kind); + expect(record.toString(), contains('[REDACTED]')); + expect(record.toString(), isNot(contains('access-secret'))); + expect(record.toString(), isNot(contains('abcd-efgh'))); + expect(record.toString(), isNot(contains('nextcloud-app-secret'))); + } + }, + ); + + test('typed reads reject a credential from another provider', () async { + final store = InMemorySecretStore(); + await store.saveOAuthTokenSet( + 'account-1', + BusyProvider.google, + _tokenSet(), + ); + + await expectLater( + store.readOAuthTokenSet('account-1', BusyProvider.microsoft), + throwsA( + isA().having( + (error) => error.actualProvider, + 'actualProvider', + BusyProvider.google, + ), + ), + ); + }); + + test('legacy OAuth keys are deleted only after verified migration', () async { + final storage = _MemorySecureStorage({ + 'busymax.oauth.account-1.access_token': 'legacy-access', + 'busymax.oauth.account-1.refresh_token': 'legacy-refresh', + 'busymax.oauth.account-1.expires_at_utc': '2026-08-08T01:00:00.000Z', + 'busymax.oauth.account-1.token_type': 'Bearer', + 'busymax.oauth.account-1.scope': 'scope-a scope-b', + SecureSecretStore.legacyActiveAccountKey: 'account-1', + }); + final store = SecureSecretStore(storage); + + expect( + await store.migrateLegacyOAuthCredential( + 'account-1', + BusyProvider.google, + ), + isTrue, + ); + final tokenSet = await store.readOAuthTokenSet( + 'account-1', + BusyProvider.google, + ); + expect(tokenSet?.accessToken, 'legacy-access'); + expect(tokenSet?.refreshToken, 'legacy-refresh'); + expect(tokenSet?.scopes, {'scope-a', 'scope-b'}); + expect( + storage.values.keys, + isNot(contains('busymax.oauth.account-1.access_token')), + ); + expect(storage.values.keys, contains('busymax.secret.account-1.v1')); + + expect(await store.readActiveAccountId(), 'account-1'); + expect( + storage.values.keys, + isNot(contains(SecureSecretStore.legacyActiveAccountKey)), + ); + expect(storage.values[SecureSecretStore.activeAccountKey], 'account-1'); + }); + + test( + 'portal encrypted secret store does not write plaintext credentials', () async { final storageFile = File('${tempDir.path}/oauth-tokens.v1.json'); final portal = _FakeSecretPortalClient( const PortalSecret(bytes: _secretBytes, token: 'portal-token'), ); - final store = PortalEncryptedOAuthTokenStore( + final store = PortalEncryptedSecretStore( portalClient: portal, storageFile: storageFile, ); final tokenSet = _tokenSet(); - await store.saveTokenSet('account-1', tokenSet); + await store.saveOAuthTokenSet('account-1', BusyProvider.google, tokenSet); await store.setActiveAccountId('account-1'); final rawFile = await storageFile.readAsString(); @@ -75,8 +166,13 @@ void main() { expect(rawFile, isNot(contains('id-secret'))); expect(rawFile, contains('ciphertext')); expect(rawFile, contains('portal-token')); + expect((await storageFile.stat()).mode & 0x1ff, 0x180); + expect((await storageFile.parent.stat()).mode & 0x1ff, 0x1c0); - final restoredStore = PortalEncryptedOAuthTokenStore( + chmod(storageFile.path, '664'); + chmod(storageFile.parent.path, '775'); + + final restoredStore = PortalEncryptedSecretStore( portalClient: _FakeSecretPortalClient( const PortalSecret(bytes: _secretBytes, token: 'portal-token'), ), @@ -84,16 +180,21 @@ void main() { ); expect(await restoredStore.readActiveAccountId(), 'account-1'); - final restored = await restoredStore.readTokenSet('account-1'); + final restored = await restoredStore.readOAuthTokenSet( + 'account-1', + BusyProvider.google, + ); expect(restored?.accessToken, 'access-secret'); expect(restored?.refreshToken, 'refresh-secret'); expect(restored?.idToken, 'id-secret'); expect(restored?.scopes, {'scope-a', 'scope-b'}); + expect((await storageFile.stat()).mode & 0x1ff, 0x180); + expect((await storageFile.parent.stat()).mode & 0x1ff, 0x1c0); }, ); test('portal encrypted token store maps portal failures', () async { - final store = PortalEncryptedOAuthTokenStore( + final store = PortalEncryptedSecretStore( portalClient: _ThrowingSecretPortalClient( const SecretPortalException( code: 'PortalUserCancelled', @@ -106,16 +207,12 @@ void main() { await expectLater( store.setActiveAccountId('account-1'), throwsA( - isA() - .having( - (error) => error.code, - 'code', - 'OAuthSecureStorageUnavailable', - ) + isA() + .having((error) => error.code, 'code', 'SecretStoreUnavailable') .having( (error) => error.message, 'message', - secureTokenStorageUnavailableMessage, + secretStorageUnavailableMessage, ), ), ); @@ -251,3 +348,51 @@ class _ThrowingSecureStorage extends FlutterSecureStorage { throw error; } } + +class _MemorySecureStorage extends FlutterSecureStorage { + _MemorySecureStorage(Map initial) : values = {...initial}; + + final Map values; + + @override + Future read({ + required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async => values[key]; + + @override + Future write({ + required String key, + required String? value, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async { + if (value == null) { + values.remove(key); + } else { + values[key] = value; + } + } + + @override + Future delete({ + required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + }) async { + values.remove(key); + } +} diff --git a/test/google_tasks/oauth/token_exchange_test.dart b/test/google_tasks/oauth/token_exchange_test.dart index 50772bb..0a7e69d 100644 --- a/test/google_tasks/oauth/token_exchange_test.dart +++ b/test/google_tasks/oauth/token_exchange_test.dart @@ -7,9 +7,10 @@ import 'package:logging/logging.dart'; import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/google_tasks/api/google_tasks_api_surface.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_service.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; void main() { test('fetchUserInfo reads Google profile from OpenID userinfo', () async { @@ -27,7 +28,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -68,7 +69,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -111,7 +112,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -134,7 +135,7 @@ void main() { test( 'Google sign-in requests incremental auth and does not assume missing granted scopes', () async { - final tokenStore = InMemoryOAuthTokenStore(); + final tokenStore = InMemorySecretStore(); final loopbackFlow = _FakeOAuthLoopbackFlow( callback: const OAuthCallbackResult(code: 'code', scope: null), ); @@ -157,7 +158,10 @@ void main() { ); final result = await service.signIn(); - final storedTokenSet = await tokenStore.readTokenSet(result.accountId); + final storedTokenSet = await tokenStore.readOAuthTokenSet( + result.accountId, + BusyProvider.google, + ); expect(loopbackFlow.extraAuthorizationParameters, { 'include_granted_scopes': 'true', @@ -189,7 +193,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: loopbackFlow, nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -222,7 +226,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -268,7 +272,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -296,7 +300,7 @@ void main() { 400, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -343,7 +347,7 @@ void main() { 400, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -398,7 +402,7 @@ void main() { 400, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -431,7 +435,7 @@ void main() { posted = true; return http.Response('{}', 200); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -461,7 +465,7 @@ void main() { posted = true; return http.Response('{}', 200); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -491,7 +495,7 @@ void main() { posted = true; return http.Response('{}', 200); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -531,7 +535,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -566,7 +570,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -594,7 +598,7 @@ void main() { 200, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); @@ -609,13 +613,15 @@ void main() { ); test('signOutAccount clears only requested Google account', () async { - final tokenStore = InMemoryOAuthTokenStore(); - await tokenStore.saveTokenSet( + final tokenStore = InMemorySecretStore(); + await tokenStore.saveOAuthTokenSet( 'google-a', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet, ); - await tokenStore.saveTokenSet( + await tokenStore.saveOAuthTokenSet( 'google-b', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet.copyWith(accessToken: 'access-b'), ); await tokenStore.setActiveAccountId('google-b'); @@ -628,25 +634,34 @@ void main() { await service.clearLocalSession(accountId: 'google-a'); - expect(await tokenStore.readTokenSet('google-a'), isNull); - expect(await tokenStore.readTokenSet('google-b'), isNotNull); + expect( + await tokenStore.readOAuthTokenSet('google-a', BusyProvider.google), + isNull, + ); + expect( + await tokenStore.readOAuthTokenSet('google-b', BusyProvider.google), + isNotNull, + ); expect(await tokenStore.readActiveAccountId(), 'google-b'); }); test( 'revokeAndSignOutAccount clears only requested Google account', () async { - final tokenStore = InMemoryOAuthTokenStore(); - await tokenStore.saveTokenSet( + final tokenStore = InMemorySecretStore(); + await tokenStore.saveOAuthTokenSet( 'google-a', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet, ); - await tokenStore.saveTokenSet( + await tokenStore.saveOAuthTokenSet( 'google-b', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet.copyWith(accessToken: 'access-b'), ); - await tokenStore.saveTokenSet( + await tokenStore.saveOAuthTokenSet( 'microsoft:m', + BusyProvider.microsoft, const OAuthTokenSetFixture().tokenSet.copyWith( accessToken: 'ms-access', ), @@ -667,9 +682,21 @@ void main() { expect(captured.url.queryParameters, isEmpty); expect(Uri.splitQueryString(captured.body)['token'], 'refresh'); - expect(await tokenStore.readTokenSet('google-a'), isNull); - expect(await tokenStore.readTokenSet('google-b'), isNotNull); - expect(await tokenStore.readTokenSet('microsoft:m'), isNotNull); + expect( + await tokenStore.readOAuthTokenSet('google-a', BusyProvider.google), + isNull, + ); + expect( + await tokenStore.readOAuthTokenSet('google-b', BusyProvider.google), + isNotNull, + ); + expect( + await tokenStore.readOAuthTokenSet( + 'microsoft:m', + BusyProvider.microsoft, + ), + isNotNull, + ); expect(await tokenStore.readActiveAccountId(), 'google-b'); }, ); @@ -677,9 +704,10 @@ void main() { test( 'revokeAuthorization reports non-success without clearing credentials', () async { - final tokenStore = InMemoryOAuthTokenStore(); - await tokenStore.saveTokenSet( + final tokenStore = InMemorySecretStore(); + await tokenStore.saveOAuthTokenSet( 'google-a', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet, ); final service = OAuthService( @@ -700,22 +728,28 @@ void main() { ), ); - expect(await tokenStore.readTokenSet('google-a'), isNotNull); + expect( + await tokenStore.readOAuthTokenSet('google-a', BusyProvider.google), + isNotNull, + ); }, ); test('refresh 400 clears only account being refreshed', () async { - final tokenStore = InMemoryOAuthTokenStore(); - await tokenStore.saveTokenSet( + final tokenStore = InMemorySecretStore(); + await tokenStore.saveOAuthTokenSet( 'google-a', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet, ); - await tokenStore.saveTokenSet( + await tokenStore.saveOAuthTokenSet( 'google-b', + BusyProvider.google, const OAuthTokenSetFixture().tokenSet.copyWith(accessToken: 'access-b'), ); - await tokenStore.saveTokenSet( + await tokenStore.saveOAuthTokenSet( 'microsoft:m', + BusyProvider.microsoft, const OAuthTokenSetFixture().tokenSet.copyWith(accessToken: 'ms-access'), ); await tokenStore.setActiveAccountId('google-b'); @@ -739,9 +773,18 @@ void main() { throwsA(isA()), ); - expect(await tokenStore.readTokenSet('google-a'), isNull); - expect(await tokenStore.readTokenSet('google-b'), isNotNull); - expect(await tokenStore.readTokenSet('microsoft:m'), isNotNull); + expect( + await tokenStore.readOAuthTokenSet('google-a', BusyProvider.google), + isNull, + ); + expect( + await tokenStore.readOAuthTokenSet('google-b', BusyProvider.google), + isNotNull, + ); + expect( + await tokenStore.readOAuthTokenSet('microsoft:m', BusyProvider.microsoft), + isNotNull, + ); expect(await tokenStore.readActiveAccountId(), 'google-b'); }); @@ -757,7 +800,7 @@ void main() { 400, ); }), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(), nowUtc: () => DateTime.utc(2026, 6, 4), ); diff --git a/test/microsoft_todo/api/microsoft_todo_api_client_test.dart b/test/microsoft_todo/api/microsoft_todo_api_client_test.dart index 824ccb2..6dc4702 100644 --- a/test/microsoft_todo/api/microsoft_todo_api_client_test.dart +++ b/test/microsoft_todo/api/microsoft_todo_api_client_test.dart @@ -83,6 +83,55 @@ void main() { expect(requests[3].url.path, '/v1.0/me/todo/lists/list-1/tasks/task-1'); }); + test('checklist methods use the task child endpoints and bodies', () async { + final requests = []; + final client = _client((request) { + requests.add(request); + if (request.method == 'DELETE') return http.Response('', 204); + if (request.method == 'GET') { + return _json({ + 'value': [ + {'id': 'step-1', 'displayName': 'Step', 'isChecked': false}, + ], + }); + } + return _json({ + 'id': 'step-1', + ...jsonDecode(request.body) as Map, + }); + }); + + await client.listChecklistItemsPage(taskListId: 'list-1', taskId: 'task-1'); + await client.createChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + body: {'displayName': 'Step'}, + ); + await client.updateChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + checklistItemId: 'step-1', + patch: {'isChecked': true}, + ); + await client.deleteChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + checklistItemId: 'step-1', + ); + + const path = '/v1.0/me/todo/lists/list-1/tasks/task-1/checklistItems'; + expect(requests[0].method, 'GET'); + expect(requests[0].url.path, path); + expect(requests[1].method, 'POST'); + expect(requests[1].url.path, path); + expect(jsonDecode(requests[1].body), {'displayName': 'Step'}); + expect(requests[2].method, 'PATCH'); + expect(requests[2].url.path, '$path/step-1'); + expect(jsonDecode(requests[2].body), {'isChecked': true}); + expect(requests[3].method, 'DELETE'); + expect(requests[3].url.path, '$path/step-1'); + }); + test('delta and paging use full stored URLs unchanged', () async { final urls = []; final client = _client((request) { @@ -146,7 +195,7 @@ void main() { }); } -MicrosoftTodoApiClient _client( +MicrosoftTodoRestApiClient _client( http.Response Function(http.Request request) handler, ) { return MicrosoftTodoRestApiClient( diff --git a/test/microsoft_todo/api/microsoft_todo_api_models_test.dart b/test/microsoft_todo/api/microsoft_todo_api_models_test.dart index 265421e..afe2899 100644 --- a/test/microsoft_todo/api/microsoft_todo_api_models_test.dart +++ b/test/microsoft_todo/api/microsoft_todo_api_models_test.dart @@ -73,4 +73,26 @@ void main() { expect(list.removed, isTrue); expect(list.removedReason, 'deleted'); }); + + test('parses checklist items and their completion timestamps', () { + final page = MicrosoftTodoChecklistItemsPageDto.fromJson({ + '@odata.nextLink': 'https://graph.microsoft.com/v1.0/next', + 'value': [ + { + 'id': 'step-1', + 'displayName': 'Book venue', + 'isChecked': true, + 'createdDateTime': '2026-06-01T10:00:00Z', + 'checkedDateTime': '2026-06-02T11:00:00Z', + }, + ], + }); + + expect(page.nextLink, 'https://graph.microsoft.com/v1.0/next'); + expect(page.items.single.id, 'step-1'); + expect(page.items.single.displayName, 'Book venue'); + expect(page.items.single.isChecked, isTrue); + expect(page.items.single.createdDateTime, '2026-06-01T10:00:00Z'); + expect(page.items.single.checkedDateTime, '2026-06-02T11:00:00Z'); + }); } diff --git a/test/microsoft_todo/api/microsoft_todo_google_tasks_adapter_test.dart b/test/microsoft_todo/api/microsoft_todo_task_remote_client_test.dart similarity index 66% rename from test/microsoft_todo/api/microsoft_todo_google_tasks_adapter_test.dart rename to test/microsoft_todo/api/microsoft_todo_task_remote_client_test.dart index 9e82eb0..f942be9 100644 --- a/test/microsoft_todo/api/microsoft_todo_google_tasks_adapter_test.dart +++ b/test/microsoft_todo/api/microsoft_todo_task_remote_client_test.dart @@ -1,17 +1,17 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_error.dart'; -import 'package:busymax/src/google_tasks/api/google_tasks_api_models.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_error.dart'; +import 'package:busymax/src/features/tasks/domain/task_remote_models.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_client.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_error.dart'; import 'package:busymax/src/microsoft_todo/api/microsoft_todo_api_models.dart'; -import 'package:busymax/src/microsoft_todo/api/microsoft_todo_google_tasks_adapter.dart'; +import 'package:busymax/src/microsoft_todo/api/microsoft_todo_task_remote_client.dart'; void main() { test( - 'create task maps Google-shaped fields to Microsoft Graph body', + 'create task maps neutral mutation fields to Microsoft Graph body', () async { final client = _FakeMicrosoftTodoApiClient(); - final adapter = MicrosoftTodoGoogleTasksAdapter( + final adapter = MicrosoftTodoTaskRemoteClient( client: client, defaultTimeZone: 'America/Vancouver', nowUtc: () => DateTime.utc(2026, 6, 6, 18), @@ -47,7 +47,7 @@ void main() { ); test('unsupported Microsoft move and clear completed are blocked', () async { - final adapter = MicrosoftTodoGoogleTasksAdapter( + final adapter = MicrosoftTodoTaskRemoteClient( client: _FakeMicrosoftTodoApiClient(), defaultTimeZone: 'UTC', ); @@ -55,7 +55,7 @@ void main() { expect( () => adapter.clearCompletedTasks('list-1'), throwsA( - isA().having( + isA().having( (error) => error.code, 'code', 'unsupported_provider_operation', @@ -64,13 +64,55 @@ void main() { ); expect( () => adapter.moveTask(sourceTaskListId: 'list-1', taskId: 'task-1'), - throwsA(isA()), + throwsA(isA()), ); }); + test( + 'maps Microsoft checklist children without flattening them as tasks', + () async { + final client = _FakeMicrosoftTodoApiClient(); + final adapter = MicrosoftTodoTaskRemoteClient( + client: client, + defaultTimeZone: 'UTC', + ); + + final page = await adapter.listChecklistItemsPage( + taskListId: 'list-1', + taskId: 'task-1', + ); + final created = await adapter.createChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + title: 'Created step', + ); + final updated = await adapter.updateChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + checklistItemId: 'step-1', + completed: true, + ); + await adapter.deleteChecklistItem( + taskListId: 'list-1', + taskId: 'task-1', + checklistItemId: 'step-1', + ); + + expect(page.items.single.title, 'Existing step'); + expect(created.title, 'Created step'); + expect(client.createdChecklistBody, { + 'displayName': 'Created step', + 'isChecked': false, + }); + expect(updated.completed, isTrue); + expect(client.updatedChecklistPatch, {'isChecked': true}); + expect(client.deletedChecklistItemId, 'step-1'); + }, + ); + test('patch sends Microsoft dateTime values with timeZone', () async { final client = _FakeMicrosoftTodoApiClient(); - final adapter = MicrosoftTodoGoogleTasksAdapter( + final adapter = MicrosoftTodoTaskRemoteClient( client: client, defaultTimeZone: 'UTC', ); @@ -112,7 +154,7 @@ void main() { 'completedDateTime patch preserves Microsoft wall-clock timezone', () async { final client = _FakeMicrosoftTodoApiClient(); - final adapter = MicrosoftTodoGoogleTasksAdapter( + final adapter = MicrosoftTodoTaskRemoteClient( client: client, defaultTimeZone: 'UTC', ); @@ -151,7 +193,7 @@ void main() { message: 'Access denied by policy.', rawJson: rawJson, ); - final adapter = MicrosoftTodoGoogleTasksAdapter( + final adapter = MicrosoftTodoTaskRemoteClient( client: client, defaultTimeZone: 'UTC', ); @@ -163,7 +205,7 @@ void main() { patch: const TaskPatch.fields({'title': 'Updated'}), ), throwsA( - isA() + isA() .having((error) => error.statusCode, 'statusCode', 403) .having( (error) => error.code, @@ -175,17 +217,80 @@ void main() { 'message', 'Access denied by policy.', ) - .having((error) => error.rawJson, 'rawJson', rawJson), + .having( + (error) => error.providerDetails, + 'providerDetails', + rawJson, + ), ), ); }); } -class _FakeMicrosoftTodoApiClient implements MicrosoftTodoApiClient { +class _FakeMicrosoftTodoApiClient + implements MicrosoftTodoApiClient, MicrosoftTodoChecklistApiClient { var createdTaskListId = ''; var createdTaskBody = {}; var updatedTaskPatch = {}; MicrosoftTodoApiError? updateTaskError; + var createdChecklistBody = {}; + var updatedChecklistPatch = {}; + String? deletedChecklistItemId; + + @override + Future listChecklistItemsPage({ + required String taskListId, + required String taskId, + String? nextLink, + }) async { + return MicrosoftTodoChecklistItemsPageDto.fromJson({ + 'value': [ + { + 'id': 'step-1', + 'displayName': 'Existing step', + 'isChecked': false, + 'createdDateTime': '2026-06-01T10:00:00Z', + }, + ], + }); + } + + @override + Future createChecklistItem({ + required String taskListId, + required String taskId, + required Map body, + }) async { + createdChecklistBody = body; + return MicrosoftTodoChecklistItemDto.fromJson({ + 'id': 'created-step', + ...body, + }); + } + + @override + Future updateChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + required Map patch, + }) async { + updatedChecklistPatch = patch; + return MicrosoftTodoChecklistItemDto.fromJson({ + 'id': checklistItemId, + 'displayName': 'Existing step', + 'isChecked': patch['isChecked'] ?? false, + }); + } + + @override + Future deleteChecklistItem({ + required String taskListId, + required String taskId, + required String checklistItemId, + }) async { + deletedChecklistItemId = checklistItemId; + } @override Future createTask({ diff --git a/test/microsoft_todo/oauth/microsoft_oauth_service_test.dart b/test/microsoft_todo/oauth/microsoft_oauth_service_test.dart index 109f29c..fb393ae 100644 --- a/test/microsoft_todo/oauth/microsoft_oauth_service_test.dart +++ b/test/microsoft_todo/oauth/microsoft_oauth_service_test.dart @@ -5,8 +5,8 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/google_tasks/oauth/oauth_loopback_flow.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_models.dart'; -import 'package:busymax/src/google_tasks/oauth/oauth_token_store.dart'; +import 'package:busymax/src/core/auth/oauth_models.dart'; +import 'package:busymax/src/core/secrets/secret_store.dart'; import 'package:busymax/src/microsoft_todo/oauth/microsoft_oauth_service.dart'; void main() { @@ -148,7 +148,7 @@ MicrosoftOAuthService _service( oauthRevocationEndpoint: 'https://oauth2.googleapis.com/revoke', ), httpClient: MockClient(handler), - tokenStore: InMemoryOAuthTokenStore(), + tokenStore: InMemorySecretStore(), loopbackFlow: OAuthLoopbackFlow(authorizationLauncher: (_) async => true), nowUtc: () => DateTime.utc(2026, 6, 6), ); diff --git a/test/providers/busy_provider_test.dart b/test/providers/busy_provider_test.dart new file mode 100644 index 0000000..2977345 --- /dev/null +++ b/test/providers/busy_provider_test.dart @@ -0,0 +1,143 @@ +import 'package:busymax/src/db/app_database.dart'; +import 'package:busymax/src/providers/account_authority.dart'; +import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('provider identity has exactly four stable storage values', () { + expect(BusyProvider.values, [ + BusyProvider.google, + BusyProvider.microsoft, + BusyProvider.appleICloud, + BusyProvider.nextcloud, + ]); + expect(BusyProvider.values.map((provider) => provider.storageValue), [ + 'google', + 'microsoft', + 'apple_icloud', + 'nextcloud', + ]); + }); + + test('stored provider parsing is total and never falls back', () { + expect( + BusyProviderCodec.parseStorageValue('google'), + isA().having( + (result) => result.value, + 'value', + BusyProvider.google, + ), + ); + expect( + BusyProviderCodec.parseStorageValue('GOOGLE'), + isA(), + ); + expect( + BusyProviderCodec.parseStorageValue('caldav'), + isA(), + ); + expect( + () => BusyProviderCodec.requireStorageValue(null), + throwsA(isA()), + ); + }); + + test('authorities are deterministic and provider-specific', () { + expect( + normalizeAccountAuthority(BusyProvider.google, authority: 'ignored'), + googleAccountAuthority, + ); + expect( + normalizeAccountAuthority(BusyProvider.microsoft, tenantId: 'TENANT-ID'), + '$microsoftAuthorityOrigin/tenant-id', + ); + expect( + normalizeAccountAuthority(BusyProvider.appleICloud), + appleICloudAccountAuthority, + ); + expect( + normalizeAccountAuthority( + BusyProvider.nextcloud, + authority: 'https://Cloud.Example.test/nextcloud///', + ), + 'https://cloud.example.test/nextcloud', + ); + expect( + normalizeProviderAccountId( + BusyProvider.appleICloud, + ' User@Example.COM ', + ), + 'user@example.com', + ); + }); + + test('Nextcloud authority rejects unsafe or ambiguous server values', () { + for (final value in [ + 'http://cloud.example.test', + 'https://user@cloud.example.test', + 'https://cloud.example.test/path?credential=value', + 'https://cloud.example.test/path#fragment', + 'not a URI', + ]) { + expect( + () => normalizeNextcloudServerAuthority(value), + throwsA(isA()), + reason: value, + ); + } + }); + + test('effective collection capabilities require both ACL and component', () { + const readOnly = CollectionCapabilities( + canRead: true, + supportsEvents: true, + supportsTasks: true, + ); + expect(readOnly.isReadOnly, isTrue); + expect(readOnly.canCreateEvent, isFalse); + expect(readOnly.canUpdateTask, isFalse); + + const writableEvents = CollectionCapabilities( + canRead: true, + canWriteContent: true, + canAddMembers: true, + canDeleteMembers: true, + supportsEvents: true, + ); + expect(writableEvents.canCreateEvent, isTrue); + expect(writableEvents.canDeleteEvent, isTrue); + expect(writableEvents.canCreateTask, isFalse); + }); + + test('same Nextcloud login is unique per normalized authority', () async { + final database = AppDatabase(NativeDatabase.memory()); + addTearDown(database.close); + const now = '2026-08-08T00:00:00.000Z'; + + Future insert(String id, String authority) { + return database + .into(database.accounts) + .insert( + AccountsCompanion.insert( + id: id, + provider: 'nextcloud', + authority: authority, + providerAccountId: 'alex', + credentialKind: 'nextcloud_app_password', + createdAtUtc: now, + updatedAtUtc: now, + ), + ); + } + + await insert('account-a', 'https://one.example.test'); + await insert('account-b', 'https://two.example.test'); + await expectLater( + insert('account-c', 'https://one.example.test'), + throwsA(anything), + ); + expect(await database.select(database.accounts).get(), hasLength(2)); + }); +} diff --git a/test/smoke_test.dart b/test/smoke_test.dart index da89cb8..adee234 100644 --- a/test/smoke_test.dart +++ b/test/smoke_test.dart @@ -6,7 +6,9 @@ import 'package:busymax/src/config/build_config.dart'; import 'package:busymax/src/db/app_database.dart'; void main() { - testWidgets('missing OAuth client ID screen is shown', (tester) async { + testWidgets('CalDAV setup remains available without OAuth client IDs', ( + tester, + ) async { final database = AppDatabase.memoryForTests(); addTearDown(database.close); @@ -37,16 +39,12 @@ void main() { expect(find.text('Connect accounts'), findsOneWidget); expect(find.text('Add Google account'), findsOneWidget); expect(find.text('Add Microsoft account'), findsOneWidget); + expect(find.text('Add Apple iCloud Calendar account'), findsOneWidget); + expect(find.text('Add Nextcloud account'), findsOneWidget); expect(find.textContaining('GOOGLE_OAUTH_CLIENT_ID'), findsOneWidget); expect( - find.text( - 'Connect Google and Microsoft accounts to sync calendars and tasks.', - ), - findsWidgets, - ); - expect( - find.textContaining('Add all Google and Microsoft accounts'), - findsNothing, + find.text('Connect calendars and tasks from one of these providers.'), + findsOneWidget, ); expect(find.text('Accounts'), findsNothing); expect(find.textContaining('sync task.'), findsNothing); From 2daf8a6b7ef3fd9bd48d215d08ffd1ef11006c19 Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 10 Aug 2026 15:24:30 -0700 Subject: [PATCH 4/5] Document provider setup and compatibility --- README.md | 46 ++++++- docs/apple_icloud_setup.md | 67 ++++++++++ docs/beta_snap_release.md | 31 +++++ docs/google_setup.md | 101 +++++---------- docs/icalendar_data_model.md | 105 +++++++++++++++ docs/live_provider_testing.md | 121 ++++++++++++++++++ docs/microsoft_setup.md | 45 +++---- docs/nextcloud_setup.md | 102 +++++++++++++++ docs/provider_support_matrix.md | 77 +++++++++++ .../account_provider_selection.png | Bin 0 -> 32738 bytes third_party/README.md | 38 +++++- tools/README.md | 3 +- 12 files changed, 630 insertions(+), 106 deletions(-) create mode 100644 docs/apple_icloud_setup.md create mode 100644 docs/icalendar_data_model.md create mode 100644 docs/live_provider_testing.md create mode 100644 docs/nextcloud_setup.md create mode 100644 docs/provider_support_matrix.md create mode 100644 docs/screenshots/account_provider_selection.png diff --git a/README.md b/README.md index 2dfc930..14ef73a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ BusyMax is a Linux desktop calendar and task manager built with Flutter. -It brings calendar events and tasks into a native-feeling Linux desktop interface, with support for `Google Calendar`, `Google Tasks`, `Microsoft Calendar`, and `Microsoft To Do`. +It brings calendar events and tasks into a native-feeling Linux desktop +interface. BusyMax connects directly to Google, Microsoft, Apple iCloud +Calendar, and Nextcloud. Apple Reminders is not supported. [![busymax](https://snapcraft.io/busymax/badge.svg)](https://snapcraft.io/busymax) @@ -20,10 +22,18 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac - Linux desktop app built with Flutter. - Calendar views for day, week, month, year, and agenda planning. -- Task creation with lists, due dates, reminders, and repeat options. -- Event editing with calendar selection, time controls, repeat rules, and reminders. +- Task creation with lists, start/due dates, reminders, repeat options, + subtasks, status, progress, priority, categories, location, and URL. +- Nextcloud task-list creation/rename/delete, recursive task duplicate/delete, + raw iCalendar export, clear-completed, and cross-list subtree moves. +- Event editing with calendar selection, time controls, repeat rules, and + reminders. - Tray shortcut for opening the main Agenda view. -- Integrations with Google Calendar, Google Tasks, Microsoft Calendar, and Microsoft To Do. +- Direct integrations with Google Calendar, Google Tasks, Microsoft Calendar, + Microsoft To Do, Apple iCloud Calendar, Nextcloud Calendar, and Nextcloud + Tasks. +- Offline-first local cache, conditional DAV writes, recurrence exceptions, + alarms, and explicit conflict handling for CalDAV accounts. ## Screenshots @@ -59,6 +69,12 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac
+

+ BusyMax account provider selection +

+

BusyMax year view

@@ -71,12 +87,20 @@ It brings calendar events and tasks into a native-feeling Linux desktop interfac ## Prerequisites -- Flutter: https://docs.flutter.dev/install +- [Flutter SDK](https://docs.flutter.dev/install) - GTK 3 and libhandy development packages (`libgtk-3-dev` and `libhandy-1-dev` on Ubuntu/Debian) -- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`, see [Google Setup](docs/google_setup.md) +- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`; see + [Google setup](docs/google_setup.md) - `MICROSOFT_OAUTH_CLIENT_ID`, see [Microsoft Setup](docs/microsoft_setup.md) +Apple and Nextcloud do not require compile-time client credentials: + +- [Apple iCloud Calendar setup](docs/apple_icloud_setup.md) requires two-factor + authentication and an Apple app-specific password. +- [Nextcloud setup](docs/nextcloud_setup.md) requires an HTTPS server and + completes authorization in the default browser. + ## Run locally Register the development launcher once so GNOME can associate BusyMax's native @@ -127,7 +151,15 @@ flutter run -d linux \ The local Snap helper accepts the same value with `--dart-define BUSYSTACK_FEEDBACK_ENDPOINT=http://127.0.0.1:8090/api/feedback`. -No API, CAPTCHA, or other private server credential is used by the desktop application. +No API, CAPTCHA, or other private server credential is used by the desktop +application. + +## Provider support + +The [provider capability matrix](docs/provider_support_matrix.md) lists the +features and limitations of each integration. Maintainers can use the +[live-provider test guide](docs/live_provider_testing.md) for opt-in Nextcloud +and iCloud integration tests. ## Build and publish the Snap diff --git a/docs/apple_icloud_setup.md b/docs/apple_icloud_setup.md new file mode 100644 index 0000000..11daab7 --- /dev/null +++ b/docs/apple_icloud_setup.md @@ -0,0 +1,67 @@ +# Apple iCloud Calendar setup + +BusyMax connects directly to Apple iCloud Calendar over CalDAV. This profile +synchronizes calendar collections and `VEVENT` resources only. It does not +connect to Apple Reminders. + +## Before connecting + +You need: + +- an Apple Account with two-factor authentication enabled; +- the email address used by that Apple Account; and +- a dedicated app-specific password for BusyMax. + +Do not enter your primary Apple Account password in BusyMax. Apple documents +app-specific passwords as the fallback for third-party apps that cannot use +Apple's account-authorization contract. BusyMax's Linux client does not use an +undocumented Apple authorization flow. + +## Create the password and connect + +1. Sign in at [account.apple.com](https://account.apple.com/). +2. Open **Sign-In and Security**, then **App-Specific Passwords**. +3. Generate a password with a recognizable label such as `BusyMax Linux`. +4. In BusyMax, open **Add account** and choose **Apple iCloud Calendar**. +5. Enter the Apple Account email and the complete generated password. BusyMax + trims accidental whitespace but otherwise treats the password as opaque. +6. Select **Connect**. BusyMax validates the credential and discovers the + account's calendars before saving it. + +BusyMax starts discovery at `https://caldav.icloud.com/`, follows only +validated Apple iCloud CalDAV destinations, and requires normal platform TLS +certificate validation. There is no invalid-certificate, HTTP, or custom +iCloud-server option. + +## Calendars and editing + +BusyMax shows discovered event calendars and derives whether each one is +writable from DAV privileges. Shared or subscribed read-only calendars remain +visible but their edit controls are disabled. Event content is cached locally +for offline viewing; offline edits to writable calendars are queued and later +sent with conditional ETag checks. + +Calendar collection creation, deletion, rename, color, and ordering are not +supported for iCloud. Invitations and scheduling changes are also not +supported. + +## Reconnect, revoke, or remove + +- If Apple rejects the credential, existing cached data and pending work stay + local. Generate a replacement app-specific password, then use **Reconnect**. +- To revoke access remotely, remove the BusyMax password at + [account.apple.com](https://account.apple.com/) under **App-Specific + Passwords**. BusyMax cannot revoke an Apple password through CalDAV. +- Removing the account from BusyMax removes its local credential, cached DAV + objects, projections, cursors, conflicts, and pending operations. It does + not revoke the remote Apple password; revoke it manually as well. +- Apple states that changing or resetting the primary Apple Account password + automatically revokes all app-specific passwords. + +Apple's current instructions are [Sign in to apps with your Apple Account +using app-specific passwords](https://support.apple.com/en-gb/102654) and +[Access your iCloud Mail, Calendar and Contacts in third-party +apps](https://support.apple.com/en-ie/121539). + +This setup is specific to Apple iCloud and cannot be used for arbitrary CalDAV +servers. diff --git a/docs/beta_snap_release.md b/docs/beta_snap_release.md index 84c0fa8..da9e748 100644 --- a/docs/beta_snap_release.md +++ b/docs/beta_snap_release.md @@ -43,6 +43,12 @@ See [Google Setup](google_setup.md) and and can be extracted, so use only native Desktop/public-client credentials. Never use server credentials or commit the JSON or generated `.snap` files. +Apple iCloud Calendar and Nextcloud do not use compile-time client secrets. +Read [Apple iCloud setup](apple_icloud_setup.md) and [Nextcloud +setup](nextcloud_setup.md). Apple requires a per-user app-specific password; +Nextcloud creates a per-client app password through Login Flow v2 in the +default browser. Never put either credential in the defines file. + ## Build From the repository root: @@ -117,11 +123,32 @@ Before upload, verify: - Desktop search shows one BusyMax launcher; the tray Agenda action opens the Agenda view in the main window. - Google and Microsoft sign-in complete successfully. +- Apple iCloud setup accepts only an Apple Account email and app-specific + password, discovers calendars over verified TLS, and reconnects after a + controlled app-password revocation. +- Nextcloud Login Flow v2 opens the system default browser, completes after the + user returns to BusyMax, preserves an installation path, and uses the + server-returned canonical credentials. - Tasks and events can be created, edited, completed, and deleted; a task created in Agenda appears immediately without manual refresh. - Accounts, settings, and data survive restart. +- The XDG Secret portal-backed encrypted credential file works while strictly + confined: connect, quit, restart the desktop session if practical, reopen, + sync, reconnect, then remove the account and confirm its local credential is + gone. +- Revoked Apple/Nextcloud credentials pause synchronization while cached data + and pending work remain visible. +- Read-only/shared DAV collections remain visible but do not expose mutation + controls; a server ACL change is enforced after refresh. +- Network, DNS, platform TLS rejection, recurrence/alarm projection, and + notifications work under confinement. - Notifications and tray actions, including opening Agenda in the main window and Quit, work. +- Upgrade a copy of data from the last released package and verify schema-5 to + schema-8 migration, existing provider credentials/cursors/pending + operations, DAV projections, and account removal/local cleanup. +- `snap/snapcraft.yaml`, metainfo, and screenshots describe exactly the tested + providers. Apple wording says iCloud Calendar, not Apple Reminders. ## Upload To Beta @@ -178,6 +205,10 @@ snap info busymax Repeat the local smoke checks against the Store-delivered revision. +Record the downloaded revision, channel, checksum, test machine, and smoke-test +result in the release record. Do not include account identities, credentials, +DAV resource paths, or calendar and task content. + Official references: [build environments](https://documentation.ubuntu.com/snapcraft/stable/reference/build-environment-options/), [upload](https://documentation.ubuntu.com/snapcraft/stable/reference/commands/upload/), and [revision management](https://documentation.ubuntu.com/snapcraft/stable/how-to/publishing/manage-revisions-and-releases/). diff --git a/docs/google_setup.md b/docs/google_setup.md index b8b5ccf..0333a34 100644 --- a/docs/google_setup.md +++ b/docs/google_setup.md @@ -1,78 +1,43 @@ -# Google Setup +# Google OAuth setup -This setup is required to get `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET` +BusyMax requires a Google desktop OAuth client. Its client ID and client secret +are supplied as `GOOGLE_OAUTH_CLIENT_ID` and +`GOOGLE_OAUTH_CLIENT_SECRET` at build time. -## 1 Enable APIs +## Create a Google Cloud project -### 1.1 Create New Project +1. Open the [Google Cloud Console](https://console.cloud.google.com/). +2. Create or select a project. +3. Enable the Google Tasks API and Google Calendar API. -Go to `Google Cloud Console`: https://console.cloud.google.com +## Configure the consent screen -Click `Open project picker` (top-left corner) and create a new project. Then, select it. +1. Open [Google Auth Platform](https://console.cloud.google.com/auth/). +2. Complete the initial setup with the application name, support email, + audience, and contact email. +3. Under **Audience**, add development accounts as test users while the app is + in testing mode. +4. Under **Data access**, add these scopes: -### 1.1 Enable Task and Calendar API + ```text + openid + https://www.googleapis.com/auth/userinfo.email + https://www.googleapis.com/auth/userinfo.profile + https://www.googleapis.com/auth/tasks + https://www.googleapis.com/auth/calendar + ``` -Search and enable the following: +The identity scopes provide the stable account identity and display label. The +Tasks and Calendar scopes allow BusyMax to synchronize and edit the +corresponding data. -- Google Tasks API -- Google Calendar API +## Create the desktop client -## 2 Google Auth Platform +1. Open **Clients** and select **Create client**. +2. Choose **Desktop app** as the application type. +3. Give the client a recognizable name. +4. Store the client ID and client secret securely and provide them to the build + as `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`. -### 2.1 Initial Setup - -Go to `Google Auth Platform`: https://console.cloud.google.com/auth/ - -Click `Get Started`. - -Enter: - -- App name: -- User support email: -- Audience: `External` -- Contact Information: - -Click `Save`. - -### 2.2 Branding - -Click `Branding` to provide additional information if needed. - -### 2.3 Audience - -Click `Audience` to add test users. While publishing status is set to "Testing", only test users are -able to access the app. - -### 2.4 Clients - -Click `Clients` -> `Create Client` and enter: - -- Application type: Desktop app -- Name: - -!!! **Important**: copy and save `Client ID` and `Client secret`. **You will no longer be able to -view or download the client secret once you close this dialog. Make sure you have copied or -downloaded the information below and securely stored it.** Use it as `GOOGLE_OAUTH_CLIENT_ID` and -`GOOGLE_OAUTH_CLIENT_SECRET`. - -#### 2.5 Data Access - -Click `Add or remove scopes` - -Check the following: - -- openid -- https://www.googleapis.com/auth/userinfo.email -- https://www.googleapis.com/auth/userinfo.profile -- https://www.googleapis.com/auth/tasks -- https://www.googleapis.com/auth/calendar - -Click `Update` and `Save`. - -Rationale: - -```text -openid/email/profile -> stable account identity and display label -tasks -> Google Tasks create/edit/delete/sync -calendar -> CalendarList, Calendars, Events, Colors, Freebusy support -``` +Use only credentials created for this desktop application. Do not commit them +to the repository. diff --git a/docs/icalendar_data_model.md b/docs/icalendar_data_model.md new file mode 100644 index 0000000..402c650 --- /dev/null +++ b/docs/icalendar_data_model.md @@ -0,0 +1,105 @@ +# iCalendar data model + +BusyMax keeps the server's complete iCalendar resource as the synchronization +authority. It does not reconstruct DAV resources from normalized event or task +rows. + +Two layers operate on each resource: + +- The document layer retains property order, unknown and duplicate properties, + parameters, sibling components, alarms, recurrence exceptions, and + `VTIMEZONE` data. Edits patch only the affected fields. +- The semantic layer projects supported `VEVENT` and `VTODO` fields into the + local database for display, search, reminders, and recurrence expansion. + +Mutated resources use CRLF line endings and fold content lines at 75 UTF-8 +octets. Untouched data remains intact, including provider-specific extensions +that BusyMax does not interpret. + +## Editable Nextcloud `VTODO` data + +The Nextcloud task editor is based on the official +[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1). +BusyMax can project and patch these properties without regenerating the rest of +the resource: + +| Data | iCalendar representation | +|---|---| +| Identity and text | `UID`, `SUMMARY`, `DESCRIPTION`, `CATEGORIES` | +| Scheduling | `DTSTART`, `DUE`, date-only, floating, UTC, or `TZID` values | +| Progress | `STATUS`, `PERCENT-COMPLETE`, `COMPLETED` | +| Details | `PRIORITY`, `LOCATION`, `URL`, `CLASS` | +| Hierarchy and order | parent `RELATED-TO`, `X-APPLE-SORT-ORDER` | +| Recurrence | `RRULE`, `RDATE`, `EXDATE`, `RECURRENCE-ID` | +| Reminders | every `VALARM` child component | +| Nextcloud UI state | `X-PINNED`, `X-OC-HIDESUBTASKS`, `X-OC-HIDECOMPLETEDSUBTASKS` | +| Bookkeeping | `CREATED`, `LAST-MODIFIED`, `DTSTAMP` | + +Status, percentage, and completion date are changed together using the same +state transitions as Nextcloud Tasks. In particular, reopening a 100-percent +complete task changes it to 99 percent when an in-progress value is needed. +Completing a parent completes its open descendants; reopening a descendant +reopens a closed ancestor. Parent deletion is queued child-first. + +The recurrence editor covers the subset exposed by Nextcloud Tasks 0.18.1: +`DAILY`, `WEEKLY`, `MONTHLY`, and `YEARLY`; `INTERVAL`; the supported `BYDAY`, +`BYMONTH`, `BYMONTHDAY`, and `BYSETPOS` combinations; and either `COUNT` or +`UNTIL`. Rules outside that subset, multiple `RRULE` properties, and detached +instances remain synchronized and preserved but are not rewritten by the +editor. Completing a recurring master creates the completed exception and +advances the master when the rule has another occurrence. + +Every alarm remains in source order. The editor adds absolute reminders and +before-start or before-due relative reminders. It applies the same editability +rules as Nextcloud Tasks 0.18.1: absolute UTC triggers and supported +start-relative triggers can be changed; due-relative, timed positive, and +other unsupported trigger forms remain visible, removable, and intact. Alarm +actions and additional properties are preserved. Removing a start or due value +with related alarms requires the user to either remove those alarms or convert +them to absolute triggers. + +## DAV task operations + +Task-object creates, updates, moves, and deletes are represented as durable +pending operations. Updates use exact ETag preconditions. A cross-list move +uses WebDAV `MOVE` with the original member name, `Depth: infinity`, and +`Overwrite: F`; descendants are moved child-first so parent relationships stay +valid. Recursive duplication assigns a new UID to the complete recurrence set +and then duplicates descendants under their corresponding new parents. Clear +completed selects closed root tasks and recursively deletes their descendants, +including cancelled root trees as Nextcloud Tasks does. + +Creating a Nextcloud task list uses extended `MKCOL` with a `VTODO`-only +supported component set, the Nextcloud default blue `#0082C9`, and the enabled +property. Rename uses `PROPPATCH`; delete and unshare use `DELETE`. The list +slug rules and collision suffixes follow Nextcloud Tasks 0.18.1. + +## Dependency decision + +No general-purpose iCalendar or CalDAV package is authoritative for this data. +The evaluated packages normalized or regenerated resources in ways that could +discard unsupported content or could not satisfy BusyMax's sync-token, +conditional-write, redirect, and parser-limit requirements. + +The `xml` package is used only for namespace-aware WebDAV XML parsing. BusyMax +adds response-size, depth, element-count, and text-size limits and rejects DTD +and entity declarations before parsing. + +## Protocol references + +- [RFC 4791](https://www.rfc-editor.org/rfc/rfc4791): CalDAV collections, + reports, object resources, and write preconditions +- [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545): iCalendar syntax and + semantics +- [RFC 4918](https://www.rfc-editor.org/rfc/rfc4918): WebDAV properties, + conditional writes, `PROPPATCH`, `MOVE`, and `DELETE` +- [RFC 5689](https://www.rfc-editor.org/rfc/rfc5689): extended `MKCOL` +- [RFC 6578](https://www.rfc-editor.org/rfc/rfc6578): WebDAV collection + synchronization +- [RFC 6764](https://www.rfc-editor.org/rfc/rfc6764): service discovery +- [RFC 7809](https://www.rfc-editor.org/rfc/rfc7809): time zones by reference + +The DAV and iCalendar test suites cover lossless field patches, task-state and +hierarchy transitions, recurring completion, alarms, custom time zones, +recursive duplicate/delete, cross-list `MOVE`, collection mutations, offline +mutation replay, and server conflict handling. diff --git a/docs/live_provider_testing.md b/docs/live_provider_testing.md new file mode 100644 index 0000000..5a0e287 --- /dev/null +++ b/docs/live_provider_testing.md @@ -0,0 +1,121 @@ +# Live provider tests + +Live integration tests are opt-in and are skipped by a normal `flutter test` +run. They create and delete remote calendars and objects, change sharing +permissions, and may revoke app passwords. Run them only against disposable QA +accounts and isolated test servers. + +Pass credentials through the test process environment. Do not commit them or +include them in logs, screenshots, or bug reports. + +## Nextcloud DAV + +Set: + +```text +BUSYMAX_NEXTCLOUD_LIVE=1 +BUSYMAX_NEXTCLOUD_LIVE_URL= +BUSYMAX_NEXTCLOUD_LIVE_USERNAME= +BUSYMAX_NEXTCLOUD_LIVE_PASSWORD= +``` + +Run: + +```bash +flutter test test/dav/nextcloud_live_integration_test.dart +``` + +The URL may use HTTP only for an isolated loopback fixture. Production +Nextcloud profiles require HTTPS. + +Set `BUSYMAX_NEXTCLOUD_LIVE_LARGE=1` to include the 128-member collection test. +The restart test uses two separate runs: + +```text +BUSYMAX_NEXTCLOUD_LIVE_RESTART_ID= +BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=prepare +``` + +Restart the server without replacing its persistent storage, then rerun with +`BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=verify`. + +## Nextcloud Login Flow v2 + +Set: + +```text +BUSYMAX_NEXTCLOUD_LOGIN_LIVE=1 +BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL= +BUSYMAX_NEXTCLOUD_LOGIN_LIVE_USERNAME= +BUSYMAX_NEXTCLOUD_LOGIN_LIVE_APP_PASSWORD= +BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT= +BUSYMAX_NEXTCLOUD_LOGIN_LIVE_BROWSER= +``` + +Run: + +```bash +flutter test test/dav/nextcloud_login_flow_live_test.dart +``` + +Run once at the server root and once through a path-prefixed installation such +as `/nextcloud`. The test revokes the app password returned by Login Flow, so +use credentials created for this purpose. + +## Nextcloud sharing + +Set: + +```text +BUSYMAX_NEXTCLOUD_SHARING_LIVE=1 +BUSYMAX_NEXTCLOUD_SHARING_LIVE_URL= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_OWNER_USERNAME= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_OWNER_PASSWORD= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_WRITER_USERNAME= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_WRITER_PASSWORD= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_READER_USERNAME= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_READER_PASSWORD= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP_MEMBER_USERNAME= +BUSYMAX_NEXTCLOUD_SHARING_LIVE_GROUP_MEMBER_PASSWORD= +``` + +Run: + +```bash +flutter test test/dav/nextcloud_sharing_live_test.dart +``` + +This test creates a calendar, changes user and group shares, verifies effective +permissions, and removes the collection. + +## Apple iCloud Calendar + +Use a dedicated Apple QA account with two-factor authentication, a BusyMax-only +app-specific password, and at least two calendars. Set: + +```text +BUSYMAX_ICLOUD_LIVE=1 +BUSYMAX_ICLOUD_LIVE_USERNAME= +BUSYMAX_ICLOUD_LIVE_PASSWORD= +BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_WRITABLE=1 +BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_READ_ONLY=1 +``` + +The two shared-calendar flags are optional and should be set only when those +fixtures exist. Run: + +```bash +flutter test test/dav/apple_icloud_live_integration_test.dart +``` + +The test covers discovery, collection permissions, conditional event writes, +date and recurrence forms, alarms, conflict behavior, credential replacement, +and local account removal. It does not replace a manual check in Apple Calendar +or iCloud.com. + +## Recording results + +Record the source revision, provider/server versions, operating environment, +test command, and result. Redact credentials, DAV resource paths, Login Flow +URLs and tokens, raw iCalendar, and user content before sharing any output. diff --git a/docs/microsoft_setup.md b/docs/microsoft_setup.md index 2921733..49268da 100644 --- a/docs/microsoft_setup.md +++ b/docs/microsoft_setup.md @@ -1,38 +1,33 @@ -# Microsoft Setup +# Microsoft OAuth setup -This setup is required to get `MICROSOFT_OAUTH_CLIENT_ID`. +BusyMax requires a Microsoft public-client application ID, supplied as +`MICROSOFT_OAUTH_CLIENT_ID` at build time. -## 1 Create app registration +## Create the application registration -Open Microsoft Entra: https://entra.microsoft.com +1. Open [Microsoft Entra](https://entra.microsoft.com/). +2. Go to **App registrations** and select **New registration**. +3. Enter an application name. +4. Select the account type that supports both organizational and personal + Microsoft accounts. +5. Add a **Public client/native mobile and desktop** redirect URI: -Go to: + ```text + http://localhost + ``` -```text -App registrations -> New registration -``` - -Enter: -- **Name**: -- **Supported account types**: `Any Entra ID Tenant + Personal Microsoft accounts` -- **Redirect**: `Public client/native mobile & desktop` with url `http://localhost` - -Click "Register". - -Copy `Application (client) ID` and use as `MICROSOFT_OAUTH_CLIENT_ID`. +6. Register the application and copy its **Application (client) ID**. Supply + that value as `MICROSOFT_OAUTH_CLIENT_ID`. -## 2 Add Microsoft Graph delegated permissions +## Add delegated Microsoft Graph permissions -Go to: - -```text -App registrations -> -> API permissions -> Add a permission -> Microsoft Graph -> Delegated permissions -``` - -Add: +Under **API permissions**, add these delegated Microsoft Graph permissions: ```text User.Read Tasks.ReadWrite Calendars.ReadWrite ``` + +Do not create or embed a client secret. BusyMax uses the public-client OAuth +flow. diff --git a/docs/nextcloud_setup.md b/docs/nextcloud_setup.md new file mode 100644 index 0000000..91199ff --- /dev/null +++ b/docs/nextcloud_setup.md @@ -0,0 +1,102 @@ +# Nextcloud Calendar and Tasks setup + +BusyMax connects directly to a selected Nextcloud server over CalDAV. It +synchronizes `VEVENT` calendars and `VTODO` task lists, including mixed +collections when the server exposes both component types. + +## Server requirements + +- Enter either the public HTTPS URL used in your browser or Nextcloud's + standard **primary CalDAV address**. For example, both + `https://cloud.example.net/nextcloud/` and + `https://cloud.example.net/nextcloud/remote.php/dav` are accepted. +- BusyMax recognizes `/remote.php/dav` (including a copied calendar-specific + DAV path), removes the DAV suffix, and starts Login Flow v2 at the actual + Nextcloud installation root. You do not need to edit a primary CalDAV + address before pasting it. +- The URL must use HTTPS, contain no username/password, and pass normal + platform certificate validation. +- This release does not support HTTP-only servers, private-CA exceptions, an + invalid-certificate toggle, or arbitrary non-Nextcloud CalDAV accounts. A + standard Nextcloud CalDAV address is supported as an input shortcut. + +Reverse proxies must preserve the installation path and return the canonical +public server URL. BusyMax accepts only same-origin Login Flow redirects that +remain within that path contract. + +## Connect with the default browser + +1. In BusyMax, open **Add account** and choose **Nextcloud**. +2. Enter the server URL or paste **Copy primary CalDAV address**, then select + **Continue in browser**. +3. BusyMax anonymously starts Nextcloud Login Flow v2 and opens the login URL + in your system's default browser. +4. Sign in there, complete any two-factor or identity-provider step, and grant + BusyMax access. +5. Return to BusyMax. It polls the one-time endpoint until Nextcloud returns + the canonical server, `loginName`, and a dedicated app password. + +BusyMax never asks for or stores the user's primary Nextcloud password. The +polling token and browser URL are treated as secrets and excluded from logs. +Nextcloud documents that the polling token is valid for 20 minutes and a +successful credential result is returned only once. Canceling the BusyMax +dialog cancels local polling. + +## Calendars, task lists, and capabilities + +BusyMax inventories the account's DAV collections and exposes event and task +views only when the advertised component set permits them. It uses ACL +privileges as the primary writability signal: + +- read-only sources remain visible and cannot be mutated; shared sources use + the privileges granted by their owner; +- calendar and task fields are enabled only when the relevant collection and + component capabilities are present; +- a task shared with the account remains editable only when the collection is + writable and its classification is `PUBLIC`; classification itself cannot be + changed by the recipient; +- unsupported scheduling or collection operations stay disabled rather than + being guessed from the server brand. + +For writable Nextcloud task lists, BusyMax supports the same task data model as +the official Nextcloud Tasks 0.18.1 editor: start and due values, all-day state, +status, percentage complete, completion time, iCalendar priority, description, +categories, location, URL, classification, multiple alarms, recurrence, +subtasks, pinning, and subtask-visibility flags. It also supports recursive +duplicate and delete, raw iCalendar export, clear-completed, task ordering, +and moving a complete task subtree between writable Nextcloud lists. + +Task lists can be created, renamed, deleted, or unshared when the server grants +the required collection privileges. These collection changes require an online +server round trip. List color/order editing, new-share administration, and the +Nextcloud trash bin are not exposed by this release. + +Calendar and task objects are cached locally for offline use. Object writes, +including cross-list task moves, are queued and use exact ETags when +connectivity returns. A server-side concurrent edit is merged only when the +changed fields are provably disjoint; otherwise BusyMax creates an explicit +conflict for the user. The complete iCalendar resource remains authoritative, +so unsupported properties and recurrence forms survive edits unchanged. + +## Reconnect, revoke, or remove + +- Use **Reconnect** to repeat Login Flow v2 after an app password is revoked. + Cached data and pending work remain available while reauthentication is + required. +- When a Nextcloud account is removed, BusyMax attempts the official + authenticated app-password deletion endpoint, then always removes its local + credential and account data. A network or server failure can prevent remote + revocation, so check **Personal settings > Security > Devices & sessions** + and revoke the BusyMax token manually when removal reports a warning. +- Revoking the BusyMax token in Nextcloud pauses synchronization without + deleting local pending work. + +Official protocol guidance: [Nextcloud Login Flow +v2](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/LoginFlow/index.html) +and [Nextcloud WebDAV +basics](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/basic.html). +Task behavior is cross-checked against the official +[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1). + +For maintainer instructions covering disposable-server and live-account tests, +see [Live provider tests](live_provider_testing.md). diff --git a/docs/provider_support_matrix.md b/docs/provider_support_matrix.md new file mode 100644 index 0000000..96d1d2f --- /dev/null +++ b/docs/provider_support_matrix.md @@ -0,0 +1,77 @@ +# Provider capabilities + +This document describes the capabilities exposed by the current source tree. +Actual write access also depends on the permissions reported by each account +and collection. Unknown capabilities fail closed; BusyMax does not infer write +access from a provider name. + +| Provider | Calendars | Tasks | Authentication | +|---|---|---|---| +| Google | Google Calendar | Google Tasks | OAuth desktop client | +| Microsoft | Microsoft Calendar | Microsoft To Do | OAuth public client | +| Apple iCloud | iCloud Calendar | Not supported; Apple Reminders is not supported | Apple app-specific password over HTTPS | +| Nextcloud | `VEVENT` collections | `VTODO` collections | Login Flow v2 in the default browser | + +## CalDAV synchronization + +| Capability | Apple iCloud Calendar | Nextcloud Calendar | Nextcloud Tasks | +|---|---|---|---| +| Collection discovery | Supported | Supported | Supported | +| Initial and incremental synchronization | Supported | Supported | Supported | +| Offline object create, edit, delete, and replay | Writable collections | Writable collections | Writable collections | +| Conditional ETag writes and explicit conflict handling | Supported | Supported | Supported | +| All-day, floating, UTC, and `TZID` date-time values | Supported | Supported | Supported | +| Recurrence rules and exceptions | Supported | Supported | Supported; see the editable subset below | +| `VALARM` preservation | Supported | Supported | Supported | +| Read-only and shared collections | Supported | Supported | Supported | +| Collection create, rename, delete, or unshare | Not supported | Not supported | Supported online when permitted | +| Collection color or order editing | Not supported | Not supported | Not supported | +| Cross-list task moves | Not applicable | Not applicable | Supported between writable Nextcloud task collections | +| Clear completed tasks | Not applicable | Not applicable | Supported | +| Invitation, scheduling, and attendee changes | Not supported | Not supported | Not applicable | +| HTTP-only or private-CA servers | Not supported | Not supported | Not supported | + +## Nextcloud task data + +BusyMax exposes the task data used by the official Nextcloud Tasks 0.18.1 +editor: + +- title, description, categories, start, due date, and all-day state; +- status, completion percentage, completion date and time, and iCalendar + priority from 0 through 9; +- location, URL, and `PUBLIC`, `CONFIDENTIAL`, or `PRIVATE` classification; +- parent-child relationships, recursive subtasks, and Nextcloud's numeric task + order, including its `CREATED`-based fallback; +- multiple reminders, including absolute and before-start/before-due triggers; +- daily, weekly, monthly, and yearly recurrence with interval, supported + day/month selectors, count, or end date; +- pinning and the Nextcloud subtask-visibility flags; +- recursive duplication, raw iCalendar export, recursive deletion, moving a + subtree between lists, and deleting Nextcloud closed root task trees. + +Completing a recurring task creates a completed recurrence instance and +advances the master task when another occurrence exists. Recurrence rules that +the editor cannot represent remain intact and read-only. BusyMax also preserves +unknown properties, parameters, duplicate properties, unsupported alarm +actions, sibling components, and time-zone definitions instead of rebuilding +the resource from the visible fields. + +The parity reference is the official +[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1). +The wire format and DAV operations follow +[RFC 5545](https://www.rfc-editor.org/rfc/rfc5545), +[RFC 4791](https://www.rfc-editor.org/rfc/rfc4791), +[RFC 4918](https://www.rfc-editor.org/rfc/rfc4918), and +[RFC 5689](https://www.rfc-editor.org/rfc/rfc5689). + +## Deliberate boundaries + +- Generic CalDAV account setup +- Apple Reminders +- Nextcloud Deck or Notes +- Nextcloud task-list color/order editing, share administration, or trash-bin + management +- Editing an individual detached task occurrence directly; synchronized + exceptions are preserved, and recurring completion is handled on the master +- EventKit or private Apple APIs +- HTTP fallback or invalid-certificate exceptions diff --git a/docs/screenshots/account_provider_selection.png b/docs/screenshots/account_provider_selection.png new file mode 100644 index 0000000000000000000000000000000000000000..40787d409d64f55f1bad9a9c5141bdcbe78d54fa GIT binary patch literal 32738 zcmeFZbyU=A`!_o1Mz=_8rIg$TQUU_fWq@>dD@aRsV}ObRigb4jC7r_<;7E6OOG^&T zdwuuwJZGJ=-m}hn&u_i!tn0B(gG+YA6&T z{QJ!CpJ(7ZyIK2%@acksjIJ|$Mm`8ouRN*Yi@#8^lJ_+{5?02}dra z`f6N0M=kTlUpF)_<#uQ01e|H6{J_5Zj*##Yg=%q%ikC!f3L8f^v}0GB=Kappa18dGXAIJVO)ka5k|-Vd_BqE633T^S@GHxbYB~- ztmOUIW4z?GXcwHAo3{Sv%Qv`xKK&I&fPDV^?=N*4O#k`z?P>0RK2vR6`RDUH4`SqV zfRyMtLY5b^NBO zudg2}F>Cp7O;c6rV|Pc#`gG$v0l(D^Oeg2RF0JlmSeWusX(ehobIzc}+e)@3u1N zINut(z4m87#M9Oo?zxVMtb5AuLqafKJ8Y|20UpF(-u30{^4gEr>B-2*RKd+EHpRKE z4!f4t|8d4WwzISI`PuVYIV$&gpH9^K@|w5m)Ok2M1-^XoLfCyx6z&8$vr?7lU5oaF z2Z2Q8Rz34SKi`f%RMbuSdA#qi&luM_jmYp(%p()2;5N zPt29ihMX{w6kJB4+}s?-uG~P8!p>xXX*H8t7G2odk~4e7r6|z zu#OKbczc3S`hKo%1cyGq;`-g*^yu%o>e(x`PfaJ=E&SH;kE}a;G@Ew(EHs@~IdwjN zG2NeUi=U`*+rf^E*kds+i`{`2sfXF)`E7@DRMVr4yqe|;Zj-ED4kPdoz|6%mR*86S zW`t6gnzvc^FEmHW4?R2|3adlZTI;@ELB}YWtCISl-e-T?D0k)?_Vi?=klA^@Rd{-| z+BsW@=}oOdQ#cdlIez=`(OP%=F_H@x-iCzqJ%D?0o{Yf&>vx=|T~w%BYQFuei<;9w zC46N9i)Ba@bWF2L_Fk*j%+ty&Nu6tnULUJclQ=Fz-?RV_ewxCyTtsBaI<3|`H7S;=P(vn>mgV=DHRl2V4!S*7PeAIhd0VTy( zK|!Rn{H+ORPk(_l8C=*xNAkql!?7CITDMib8W$@J zC%@gOpmlG~n>TO#jt_R0`s8Q&a(1i_HgJrthrQ~`w!>wtZM2L6(c=xAhBZ&IzE*NE zgc1_i!?jxFb$ekqQOD_5w_-f)iOI+&O3dVQ)iRxCe_kRdPv7y9>qBd932C4;tZvrP z{%~W0V=s_rh`*U&U|z4g%7!|H>pD=D3yXV~8h6(!?UWr3!Q@7V&%g*A`7egoSy*Jw zI}3;Nx<>}%*C&c{$gf<09Lg=tjXEz<`W4ps1yFOCMb@Qg=eLe2ICg$X$ zrMx1S{Pfv;qR7fpvzDwNxMosg;|WpEO~F&Ej;#0S%36ujBlYudhD%|^^=sVuLZoQ-^ngKSH%oR8Y9f{P-dT%kM^N<6kVsizX|B#k2x;b>kHjkBz(*Gx==#;T%0I zu8?|1wYIUhT!iJnn5~rPwbdq|UZ@f)T?ti(PS7DG^ek+=)Y`3u4#qo=1BCMx(pr;Q z)w9f6qK1sSVuhT|$29G_So#a}vpwo;1~{XY1(8z%$GWRkn*{2kYD)Jq3n!-%twnE+ zDp&npnh_UTMSsPs5@nYN`^NHx5Z3qV@Y_Hl%5DW-=f^!+iezHjAIz0pT*~HRPUnTK z+jhg54b-0t9u@{6FIrYkZQV6>I`cQLU^qk!#`lV41CNHw; zi^G}^`qSsnupdjz`K0cURQyw7UG7%+pZaaZdkEKS{}l4s`#iU&lNesQ&yDJePj9+% zi&lhPEkkC!&ePQcC%wLAcjM9f!5Y_Pb!9OqEd}=2;|gP}AJo78gYHcE5#}Mu^LRLb zwO=b*HHlA;w!-WZi!9tY|AI3*`4PI@?&?VP5k_ulJd|Dpt&z(EJuH^j0!~ApQ|l}x zyVk4MuLtsVdwkeq&XgWgWSTKC%`F`4EW;)(f?k#Ee^T9&JQX=2;FkPCft#$ z_JWP*y4>Fv$8Gu}MuK;W^a&}InBVbxyH#vh!;c?E)lO3O0k;@NmKCqm&%aISjn*b# z3a=K04VBqNQB!z{;0t{q^wiqLUu|?Di!68G{=k+A#zAiizUPf!)67wMez=a62&CE0 zcV1|Zgf>C4aA8U+f(5#CI5{ZrtR|YLCUgJ@t>y+OMi@ zS$`CR)r`M6vq5{m)gnx@i3@wwUFW%FU%&t8T7PwbtVoc#M@N#Fe!2Bd1G#d}NL+`q z#Bo`FN1}-KM%9z=LBS<}M*L6qvyUnGe1A{g$52s(m8aCoM|0YaRuvnjm^21|e@!Og zG%K^vp6FWZv0-xPe7NMZHd@_Qf(a0LwclFjt2a3B7b<15QoZ1}@6ueTJHBrsg(=a> z*LlLcyL`MvkIHpdKM73^qF$sQEIjr!b#^R27Mi8CkMW$vPKmp6z^JjIa~c^ZiT*A0^ygpX zUZoZtc76ok8qO}fw^kq4uqBf?`W*XMuX7dpOG1lAfu1XXtG+z#0Vkq`v{e~Xs=HEs zCNeT_E_+9^X|9+w=MYK3Ll}L2{Cpb2HgLB;!w}jjbZMw{F50R?$9d8e&|wz4;akvL zFy44!oat8a!s?Cfh)0s zE}5$9cK09r7Kczh?|vUE`s$YD-m+ZtSWR&x`{U{b(-Yy`imGN*6-oHvg^6DOmu0TU z{#^2@W6MtDBbxkf`*p?pbS1b~KA3+s|EJxE!QKbDq zfxb4p>SVtI*;E1PP&&D6x%Q};(r7;GUXT5`=tyq>bh-I9+BQkXRKu%~yVOMz{*(9VtgWe3_ zBA>@vX|5w#!rnyL#3YTB5YT`blk>9-J9Y+YcCA)}u+>%{VT}Tc+d3+`@=r$oV8B=d zK1h#)S2YHR5!%ESz>TW_5%u_m?MN`XaDSE8wGZ*@1Ba30%iC0dM1>TGgDKhV#_M!z zmB~*U-jdI2I6H0Um=}%nMt)M?s?SE{<8|7^Fmq9?>Ob<|zk8=nvh;pfGjLwd<}hy^ zHEO_sz5PY*);VK)CM$0_r>3L+avSl^sUuI*Eup6cM|07}qsoHD_O*cG#|t7}j+1a1 z*ZV-x2lQ&4x%sl>l}T8?%@jb=tlvQ-v@X4SU(S-nFp4{hYv?SIe!s;#dc`<`ez zP-w`Y{QQxWREUd%uK&@VqqA23jX8qGnOO$S-lM74j9JRbYa3m5V>L|phL`8b*fd7m zBNN)b0YFyhtT@inkX?jcm6_WfmNC$HD*DgC+K{`lZMHzjihUO~aT5 z^fPWSJ;l3Atgdm8axN!M%-zmzxyor&_sd83mErB`&91Jl_GJ50SmU+_4<5Y0xe<&K z0n;3UW3|;&w0}%v)K!SJ4}$WRs%ml!1*v2dcImsQ{q1p{k6ka+j2n+TY(d+@c;f zxOwwt#*yon9zL`GXaT-uHVAjFvZA?b>EKOs&^xtTMu@6%zRAR7Sm(hFbhcfhIEu%t z@#oKi4Z+|7cFb z;E)g!Vq%50+WZ-SW9jMk%|%obB1nxx82BLz(W6I?V6U)oAMEd2L0h#^1X?;`&)W4L zW*lkq#K3@Vb)4egr2c;gXy#dz%{$6O|!ixHz_;|Vh1+U&+7|Li7H3P%R zw~Jh_UcIsu5*9vajpc>H{*_cu=*hQN+iR1GcPu+o;N$dYL1J<2h>-G+?yA0v5;K zZEK`I3i{_*fNDd5{Y%t>#qI*7GOxX>}l0=;sg z3m2+!GaKn);!DfR@#7fe74X6<;L-vf2g0WHljaE4_8~!`(}Mx*(m+u%u(kJq2c@MM zgZX?QpRX*w|l4)-KT736nUvBJ7gm+ylrj zaM-ZQVG8y{LNiIBA-`qk->#2Zo;-PCej@>vO7_#O`7R)~fk8pmDmAp^6#jPp;12Q3`Cj1ZUEY9&8IeIa0tw7Qs0oh$_D_S=u>bykvAu!1&2mXr%e%N zeto?t96-Km0;g_CM%oU%s6^g^xRc43WExG2J=tWSBk9Bo*pI{RfD5hiKRt2dW|Ex~ zpcHpoiFsiOeCowqynVfpNy8g>;#i)xcmYP9r!uhZW~Qg_CIDJwt%CcZPR1IyHi0$X zP#yyMJC=1+?Ytlw$~}7j(#Ef@bhMl;C9B#jh(*257Hx56&6a|}p>pw{XfAoGsf7=B zkHPB3o&iXCG!rIKIGCRm#wbx(7boIwM=mZ!84BVdt>TW2BwCUZP^5OO!Amtx#YR8p z$jx&CwK^j=0ie@F*F5EaX9xHD6olocEm4Y0<&iv~Qif<{DO^>D<(sSl%;Uj5$27(U zRB$$c;A})lBrWBt-`>>uQnimOAEg-r#a7 z)@tPhTNS@mcS9%+h=0kq&o4GWCXTwTYoaB-H~BK_yX#V~)j(llNwM#Xf%gxd4BkeN z`%-*GSjG5{5L!j;39n@i+u)Pq!)zgj?9X3AfHz3idV^rlo}CVwLFThMxJ#gy3Kofa z{C-SR1E_4x5!qe0Sy+A^tiNrD;Ir(!S7|d)SYBGp@=mKjPjgY}LjTW#1Xaz_~N&P?{r-(1l0)Yke5bJ^VA8FaY?21hqp<0x} zLjtd+U{U#-jcqAX+X&H*Sjndx#As(uC8eePe}6sSj?{o6k7r7f`k^goc1?6fAe@Y$ zA)OU7GkJgiQ=sa@LpmoXhxVl(;MPFBvzY>J#XB_U;tKZ$RAesIG)Y8XyshJa8LLpk zd^OzL-(TcsWo`1D1Dzxyh!jo}?1oWt5C)4zpgz>*BRc7{Bk02k+CLf_eSsIX+bAUn zrs~f9iB#e}|Kf^wZ4#A?UiKv9Kdo z0jIaiJYPLqiDueyNucWDYG;;WJaE`Y-}hlZ81VBz-FCeOrQZY?L`@r%%H~Fj0Lrq< z3eC|IOw4g76*30mi075 zF`iC+3&?B4bU3@|!Q(>m`b%M=n?Hd{Ls4}C$r^SHuxzDkyuetXd2t@grgEtNZTjOq z10!E9AK?IT8AH6%@i}*amQ?rkaj2Wnbgh9QuMU?>oa~t{6kP>VQHBI*Q9!(T%&_2d zv0iPD7vQQk0cRl$NXegn=BQ_fZgmL0?M}C|A5$jEn+IKu3ijh1PYnqvY5muV;R;(g zu^`(4K^$UF_SuwO;F%{Q+8t1-<+TWEGk3J8zcf7ewDw6MbEU-jH7BaEs0Td))AFy+ zk&T@Ilw4v3LhH~1~^A_y>x*QJ8Dm-=j=(e4iL)rxp+Tf&EjN0q#2p-Sot zJ7{MP@4Ry2fXIOrVAl8`Y~ZGTsC^pI#f$8EWtPw!MU5z*wGRy^Gb)AdpA+au9#Kc84tB(=ar4$C*R3GN0O2T8Poa4;V7z5;ERO#n=dqlwd#O4fA2G4N`7xVI?#*;FuIu7~m& zW7Wxf1#su|+&RVJwnYaDxXn$+xwwbHzd_mFtV;C{H6o$NH{ej??y${p4A^;e zB5wJ@pFVu}z-25RBE!^II2O#~|LK+@SRCAr(+zNl0YYUy{`$fzs^Z?T(%)PLRd;UR zR`Aw6JMb&wdATO#us*mlrX)}XosES$uFchRxXP?`HIlLpJQh+btIB^iURT``2P*g1 zm*=xpIx0z7DG>R$Xef4$bZuEWjKP*G+wZ0R>G2820$=XXXXuW(lK)CeKy(1ULZSX2 zvlRbd@U%6$qWXg+I9OUBc;AQw2^rA*^Yyx|c33StEnIg#bPPcE{`fI>dpTRHLGhE5 z6No=FCrg~dx1~d9C;?EbdxeKWeb>)bV`5}XhVx`UTGeG58_)C64hz+d11JP^R?cOB zM+IYeP{lLki%=*$1+42G9#VmO069zw$>ez;N!Rl{0x_h52z67UNb@EZCf-En~DE{QLy^)7i6ULGwO>O4yn#VT_~{^wK2Xg3<@)hrRQWq$G>>vTiUH zoXGwzPuO(4X3Z2=ukQCKO8_#^R|Z&bHBpaw;cj9w4RsClvBE*kN}!2aIh()kPk zkwHaY%xk+doedBnXDu_B3`l2$*id+Qc<=P93*-oP4@dKY9Ux~1SZ)OPX-g+`8#U-< z(0THBu}7ON06;i!Ql&Qi`MIDqL(lJBgL0>MUF~$F%<2<3ui#?m>sPiHW>|Km0i$gO zH>Id(9oCRfXAJEC$OhPF4^Y{FfBUa~6>QU^4-QTWvn zJfN_w&rCZX(u&wxO6(wg z0^uolUpK0~%r^!rmPtA_HPz%=4}D;+IZ~a(yw(a|Oxb;7@RNF*jamL>apSR8vLtrtDWH8S_eTjR`Bg#eMu&+Q)DDo zx(NN>MA_SxK+%;~f$|VgYHL0%|Ff9bWN;GhtXfa!8iwcvA`Hvz(@5{YO6ss46zi;< zxGHeN0Iak{jw_4p^g>yKb0f;Hu`_}*G& z8L*`4<@Qg^Vf_s_oc&0J;(aBX-l&7g%VCstlx!5=AxO3HD?N-Yrw#%n&1b!Gz^c^q z2dej)(XR>$jNG3@WX;c)zC-gxtBP(kkh_TyNdTwOyrs&C0IhPx2MDLECJkZGu0&3X$MY9K|Sm9o0UxEe7#TRzD)R^8hVz%!^o|)ms{n5 z8!vM~I{t2;5}KdsJG2p&2hm^!%#_=x2xt`SW|b6As}m&-ct!R;W>!kDO!_72Aa&bT zfaJcRCd~?*Q2<~Hd(qZ?Xe;|9*ge`sM#C==FPNd5xNE-ez0K?!D&SW42?t+BU5{IJzX7tB%!X0hL8tIRWWRo;^R zs82fu+EDka-m2aX02>~o$Yx@pjnCu1d$%t?)?R$gsMgJPl~j?qO(CpEa&?VC%2|I1>A3m@$D7PxL37A(_Z4W{FIDI;g z3Wz`;C_8=C@-FCAYhO%xRO6JpCIHs+@aQb z2Ztw;5YDQkyKn~qOQ_(xa&LwIA|oN`JAlHQ=SM$hGeqaZ9__XA6t-&QQ2qWqZKL*B z`S((ja9x$eX6CiyiCXs}A=CW7jPkTNNTwhS)Nj~Gtp4^}xSHhiGlXUNsy@(5qv@e9 zQTki}gle4OE=?xxqmlCgIYAn3x;~ zOKK6wZ0S~c?VwFw<{AV74z%gF$@~^Wb>_?&3R<-u*}xyKUi}gM4f2{CdS!Pk!lD1n zj-IbQZ6*eYfUeG?=}i-(Doy!iBE%UD-8_#drq%R#ec~b;Xksb%&pI2Hy~u3x4*HgX zxdf553E+R@7;kn{micOfx5J;GJd~o@QEzE3bh8(;lXcE5I5w!QWc@uTn`vxoJ(u?R z^JkDbPWEQPvI+pT#j$s*4|%%pe&M4k6{43;!*~7J#yi(yG~)KUp0JJZIM7wm=geFN#7LkOMQR z?EYAM*{R&R4}9L)%@3sgiP&`cu5bG);?+ck;)bkn-+ES|p8Yz?HE;PytiDhGcqz3z za5E^;?KaNB1X_Q-L3RvO4Gd^WwW9LNkvZ84vC~GW-lvqry|1RrWd98Q%N&En`okl^ zuN8FWq`EG4H%hADzoI`Ouy!Aw8IhvP2aHM?(FWB{P~CTaft~4shZL1UDsz-+3;UNr z_f?JL1PbBm{#0l@!v@-g25^v!TjiLNZz;yHxWuN!KUfeeIbX;+<)fyNs=?a(ba?jB zLf20Jr_l^2&J_AMW@UygZA#m}LfU6EFZ1KIM0br#?Yh#W)+F|3OLE(;4p4Ym3L3wE zES|AC?&4G=xpSWvEBW>^BGsRU^SAjgRL4Q%c79u*loucd+{MO@APgxsHi*J*f`ny2 z(g8O)vM7{MB8gp_bFLU<<72?%1z>!uWd?^k!^uKqEucDgd;KTB5Ht?s82x=f9wkcN z-&)W;I=NEC1ddoP@31zRBr^fPisMteUyX+zAsP%G?X^jUx-AUUvV zO{dFzbdlx&B^p%d&QALxg1y`4aCIXDh~M5mZ2aU{3v{@kU!F=pawq z6LeKOkBdb4F?V~#Y}IzI=~p}hJT+cn3){cmoQ$OHykajvWZ>-mP6m(G#!cYvtx99m z;sosN_)}U+p-*=~KL+H6{*K6Q@~sKPr;xoX%iZbTgLUFl!;F@V`)YY7hyn-b_A8d? zg~+!wB5^9@>ZFz#VZ1;)&}deB)IC*2n(KO{z#zLJ&GHiPOc0JD74*r0&311br`&%@ z;nfz6VrTxPwU`FX65wvn1|A}VkrosqV4rsa&W8o~D#Qi>Xk9+I0S!(DLwTkyTfQyA zZw9W_p9!jN9v*^l3!v)AzqTba6CstvMsSNdtla49UNMfr8yJ2$p+QfU;dw zDwXAzV8s|NU!Fd3fiUZNMjsUdBNA3KB#O+bufsYA$Ef#V^R?z;R(nn+Bij(*C4ITS z(&$yotu>>0f}aw30}oJjV&FD;g;QeFvl+2YwmxMLa*}lXSsetR1WDS+{iN|AN*uG` z==hHopd4F)PAYtr)sVTG#R-^CWJekM(Mwc`ZQ)EDFU`s4C*SXK+IB-~F~f&npO1wd z1RH~`&XY%$?!mPH{VMHl$t*{v`A#{i8<)P0y3v3}r=4jc=RJM$e4d}ChM&&t*s|;ad(iy5Ov)Ml9pno8q z)kuN6gE4mStJ&Ox^M{3`!R%z1qHrAwDUtS;V) z<0IW21I;3aaKZkj(e$KRr1SH=J&)QB0EHmRlC8du;xe|Qnis6nnC8*RYzEw0>po&P z2dCWKIC(V6g)z~YdqmsV*Nn@1W!4@NC#zF=+DJ3iRZ88O_IX~@^2Mz701BWq3N_HQ z|G*N&`+v_Ip#D21@n6VzI0(qf$vMIF%II|@VsjBSxeDvJc21U*>gyGh9rc!9-m`o{ z#@E|xmr$cDh=m&+6JvRdzt`5$!S(4Sm7u}dfcW+!As!%r!7r&+c3|*haYO)BbswoW zt@|0pq%DA5cz_9yC!PtYjooiaA9sQX{go?MJb?Me#Kbf-NEY0!JTkl-fJb_dmh5S! z8CV@~d_tR0FcSl8X8d`2TH5sV^h#tM?b+XI@X+JUCozs77lUcp1A{US9cowF0qztP z**|-hKj-0m3mysiJ)@?Rx?(I=V(1F0J8?XCr=5}wK#SW z6~K-W!ZkEF`0}CbL2#NZI66A&uDystY1avz{cVK2#)gvu)0PeTJBx$lP{7;m+mFui zpw=HCzbrU=CP3sb`*u_v^}ppQcv9p8{to&0UmFDaN4y#>1;~MP97KD8^Mu5X@Hs$Z zv)!ptlcDOMRsurDg8m5#@8TFWs%i|jh=T|c%G=o3=n%svv>ZTcYaFT@y|7Ewv0>bu zC*J_VpC_SlIgCH&fCERo6X+A@P<&vyUFHe!CV`8#djZZ7zW#8{doeQ_K~3sxw>8lk zfW73RIq9!liHV3nv`?sz0uw%4SsH7YvOwsyt8keP(Y!-3uZSlZVBrF2BSGwdZfI2J zQ4A?-0ValMcI`D#b_Tw|&=CyNgafqZgDVv8wgG_vL|2Re6Sx2(&A}KPE69BG<*5&jkvX zbBCBVNM9Kl833;WU%i5O^b-gzwsXLQQaLnT&}+cTDH-nOE|!lT_t|O#7dA(m<|pV^ zk3#9Jgv8VT@l6&Xd_hlt45BRp(N0hBSg6u|AxfHUi6Z#CqI$VPQ;_LS|7AefaDXf( z9dFa{<`Nhl$#b$;1X_D7JMbMfSD}KXg()?7;F^VgbCKHcSi_)=2ig+q(br{t7Kelh>`ZYO zRX~J;7_`TVL24Q@w1UJ7AkG*^;q|NY8lmPF6lb}%L!rHoNa-DfsKugu^rn=0J*Li>kcq6#NU6bW&?6n zOC)=CS0tyQ7i@7oQ=$5H=}I_Gsvk}uld;0rXfC3}MIBO8rGm@#)$!LJ2EhRmrv3@L7WQ$`l_G+;QdzxDTgX{}#k6Xm zwiBcAOuq0tr|%ug8hcJz1%+NM*9kco1iAn69K860%xEKFBy@x)MAX*%d>GK*pti;c*c&01*L<6e5(N~>0}zw1K#YMo*(a@22NJ~# z_NzSgaLQpF>);c(L3%(`OiWB*`CA`?Z2Zo8xi_~5iVEB=;Ah@|L?P~*L(4F*%qllf zP6wMgGu;U9k>3f!N~?hUT!G?wY3b+;0&)hHYDKNmuD#GUHw5{Q$^jTOQC9ZZn|cjb zJpvFJVN_DDQ&Ym|o|+)4mpO7^gwiB!qJc(6(uF8tY7` zfmE33lM1G|MtinO02=FPx?Qt?s3w>lV4JqvL_DLUfQu1^afA;a{%ZWjoaAx+`gIdX zoLuEK*LV*0cRJK$Aaio55WiK+$k!`J%a3LH@GO$wcc12)>fCH|5V87e$p{&Q|Y!l+rOEdaD6>7If?3H%H z2y+-jENt+GiUG`WkncS>j9&r>fO9NPVEtjlY8nqTI04cbu=p%exF0_d!GzUiWg*$$ z4`L%Q44OHZzW_hHJ69kEe+D-hNu$vs8KJVGE9V`^>A@a=fdiR!Bh2}4+Q4@9jwKD4SCg1CaQ~f|gLj&}Vo?3Mj zO5Xmbmwv_R)20bd0uOrlC3`c{I!@#ImF4A@*B=P}_LY*BUM3mBUBeunL0KV)&#d)% zwC|ZAgBF0)S12eNxUZr{{k#|vLzcfHq5aCIPXrz};DL%}kLV5cI-ltJ`1strhnnam zL{)wo_cJvyY31uP&?5OVf-D)M29G{S35En@Gwej31lA43R636a|rpZI|z+opg3RY@}OlW?*SX6Yt( zR-Uzfyo$=WIPOOTg3N z5(TYaF&hrj zT{nCvH+Z3A;Nz-#cC*hbvx#`hpEdvidWc+A9_Tv@ZSmUN9R_PT8U|<$Rw)_|WO$VQ zA|+b{v|4zR13)HlGrL6dAZG<$^6>!n#1(!8erSo=e7mG7sS5#8NSyX6_2=w)q zS*g3~?;tKJED3n-U8O+$0BKetgNUsb0Fv+eM7|FVO@dwcmYlius)VboB1^ATDQx{w z7oBo3Krq8-wjvKC8ppQh3R7WF!n`!%dlL*G#HFPSv_N;{tLJS7>1?q)s&ZvWxaO-? zDPM@5vg}>k84Mg_BqAdlpb>-MMDFn+KJWFgF-$!{7prq-M})!3zGgtAtrI2&yY*3& zVVb4pT|m^b&mh6ek%|JZdb1X)g(L*%=@gr+WAI5O#dCY5fpdEol^}VeAqa&hjvr)H z2fHydCo~jye1B92;5^uIZFE{BaeaE}_obwyVyWNkJ_eh#EG2OV$XXspTiz#TWor}5 zWIq_l%JReat4~Ne1ygg4gN7{e8Wd$4yw!KKp;*Sii?K4#w~I_iFif@x6QM>btaVfX zQ^3rYchzY`@DGe_iICYsOJ2g7=X>Q*&%oGRSv1IyXGJ@Wy2%EdR6(Y(9f5W{U+_SVQ_ zijcn3MhJF0Y%ew0TD=`<5)R{f1Kfe|8%Ti^UoI(0L#y2@d1vK9lV0*_`uU6JL0PIX zhAHAPQ#p;$U9}Hlj*9|#Z}hmVoNR$xh$i6~Vu=Y)<*#>(`j%aS(E<*-^Ny>&(@wE^M}z+( z-CQ3M48C8J^m5t}bcyZF-obLQ)zO@GOX?g6NQ7+be3`L%0?*Yv9_I~pz4s3-C4T?* z&#$uv5)X>n3Jr&4}Z0^%DKqefp`pR*h`-O$3b@A7>Yh1QrMS&DSQ2b1h)?7?c z=pyDY86b314A@`j%qR9N9)yolwHB9C$wl3U)HDO(9rb&-emBwGot<)J2~r673kvEe z4-mM3CF1(#S=*Gwr2$Xi1rtmSg8#m1CN~-206QrH)Po*(GmhEy;l-TXmyju9on>;jd3y zgy59IRof;g!{Ad4wX)#P@JA24fSJv*BpV^4RIk-4GQ}A_Zkg87G7m}%R8)i4Xt$@D-^y-eI)B@^@__j8@o;RYO`>w~u*;-A9Ews= zU@PEvi5+kEM9rj(R>z!q2nBGAR!v|Tk!BAqzzzBgU@3Te5u!ri9Z4`3!s%_I58?Q=azz^M*NvbT;>^?+$6g(k~JdLor*hNcJ^f zzZgBmGJ8`y%$r=!3_3IP&g2^O345#MjhL_(BI?30Ta32uzu85q8SXN17eka1!X z-Kxv#F93Q9iNcy&Z9cy>XSz@N(&HpJhemvzyz=>hG2gx0>vOIa(qM2z+mpm)S-mkZWlZ6(>t$fJ~R?J0{7lnH+7Rb&=2Jc{1m#Y zYWT+u?CJ4({O#X>vBYXCZk-SX8SHqR@l2-Ym;iHUs6F@N$~;Pk3e`JOTI zCJ!8icigGbd3mO@#V37NXi>}WMb8lpLghmu>WS+<%;7vrAN}#Q6XhqxeYucJp3C<>kTn*Au9b{6;kDmf(v4)1bdEvLTmM+NNB99Njoe9T3zx;q%1h#*AnVYpAb z(7Cg-qbdULHKNiASxk`|88EERDBm8#eItlkaTAEQ?^gW8`^8)CWkDdMiKzJ>?QIl? z@7aA~FH|+*&Jd&yOYxe1%WTrlc)EuJ$>wJ4@ad|(f326NCP}+ zF1@{C{9`M2O^U636M4^;9wiYD`X=TRQWH=&!)D(-mnsy6Vk0{*r?)Z&9x0to{d)bO z6Ln~$z)&=d-aBgU7J^N=82?JULS!Q+vLL%L>6eN zv{At2zu&?w(R>FJJTjffxJuD+@961|u=~p$eMSDq%X?SF@uSW?v_Uh~AXb{^OUQ{> z7s#RnPN$wZCKeC6jo9Mhg@}aT#?0Vh(q#w^7&k@_oM)|*-r~P%;&Q#mjocLc$n$;0E%GKGQ zIN7cA;L~SReiwIy-1~Ox6XV)OO8y}n@7)9_TUI9@klogH`nc7eH@tPCS8?KdEWLHc zPXMPA&2IKpQ$eFbVHUIP`(MfYI;Y+3>o$I@9Ic#Wkynm;*{sndwOTqRDnHGChEhDa zvI5+|69n6Z_O~yZj{~N+rwUf&m%ZJG%%YiMC6ev}P8l_eTTxnGwmK=8uy~3+Qh)Rb zQ?-Q73pbC$B7-ZfCyV*>H#&B@q#5Vx@8xcX)K`zR)jKFwP@WRTof1o*6);|aP;7i_ z)3JSc?V?u2_`xlwwsx89c&z3Wye(1tG0&l1L&rWGo#Xk7v(_j6Q}9CUH~l%>2b^%H zuO*JIiAWs2%HZmhIN1ohC2^$a7gW0Z3|Yt1i;8YjAKyo=QlOSEcoKhJTY=rZsLzhr zHKWSo2a*MKzBO&u8}=f_U$^`T0+ho<&*`|IBff%6ZvsDUZf>5B7A8Rbyn*~NusTOR zhQ4XV&z+h#`lqepy64atj=-Zl3MF|=(qx(v zFlTf(w5J66Pef$ooVN;!wq#o%%<-o!aC^{jZLF=)XfzECjfwd|(S-b&KzW9qcpB(N;K9wY)| zR0akHhK7kxN!vHCu&BWQGDCRU=%^@b(}22n4;|@ZZES3MYV$E)T|tSpyW|;fMb($Jq$#YNhkoX?5;gMb8JUI(~JztQg!X?(8RrS zMDO4Sk?NcQJ0D*1s#N{675>>3AO4p%h;2VqeI=o}2%oSssDPb7(R0H&iu^-2Cmy17 z)?MHY00CW*-%`jyqnq(MsD3`!t(*#RcXJC1?O>KpaXySzpb%!Qz#}?Q))oY@1K5}; z*mE{k0m-+O;*h~!$UN87*NX=o1K%ksRXn@g3aR9A&xJ(xqPk5uHR?a_>E8$==zjro zuSfF62i+j+?!f?b{{Ap$^=vq1cX-*}|4?7U(ruo9kL=O2N~~4;!u^hR00K)6;XeB_;_*mDkQxj=AZ763Y9P@kt3?P~f|in@uYE z`^)J5#Njv|Dt{%!zPLc0fprl%1>pnWmOpHGb~d(gkC_mvfvcKzeOmfbmyMm<_!bjj zeDJtW+qg|@f{@yv6Ywqc!G>*k)06YWS2|%VWUv`txB!fYBh8q4G`G&3_3OrLiJxGQmFwT+6n?~ERk&p{*Hg1!^H=({__oHQuUh1q_~ zob+Z4LEP{*E8*jt^c@|L;W7~gFJrnn=zRY5K;p9dn^!E^<9w7{%X)x+VYT|}AR$$^ z-7Q}^>eTWTf>5y1S(F0sMs;vUajihh-(TUr&bAFv!*~1V=^XAUNbJvk`sfB`YKSVV z37@gWt6zXBP1_cu!NS;vEAoO8&WB~G0jMuEhsw%A8~<^H^BP zm<%OD=FG$S+}h{teb%{t=i0w>u5o!xLz{a9&1bhJ$Ly{#4qFaJ=^lTP2WZc_S@i;sui~EsnO(%WH0K zF3fw^*E`-ONNV``XYQbn-auAd?n7xC#)}!G1kx!KVUT+WSWDW4bDCzChcq(n{faNy zHzw1OKbJ&+K1viaplxt<<283;4)Nu+32$%&^@2F@c8Fw_w6Jhv->BIp>Ea(j%5$qO?T*jS0G5x5qFUC=A^99IE!ZJ|8eyYOK zlX*m1TRCwL(ZC58H5FaATOAp2(d6t_(wy0k*)ijZDK7?K>Z-b!X+F%`{XiGC*r1kITIBQc3DUrM{&JV(YDq&KG3i=ND2Uu^G_Wns4O zmcmLy86kFeaO!(~N7Y_)Pft%9NdpJaAUG$THW{^AUn^${bIyAY>UB~-1|{svAKuP9 zW_Rvf3vTuMxo6t;jr|)l($#HX!us@i@3^%wlME4rL!LMKdXr<&emZt(l-)UJ)XWX< zQGP5iJFhf1}zy@UuOP_?iefx(N_&zk(TJ}V6p*7Tb37mEcS_5~Gv;({?QcT7& zC5FOmC7amZ#hSe?sO+OVJ6Qn5M;Mc`E_Qkp= zE}_%b27??RbNe>43!9W6Mv5ZQ2#sCmBakgQn$Xb*&rGz zt23HlQf(r2y0Bfh{@{wPbR{dLt&}%C?3bSJeLap=l|ZxPloU0&iKCSz9&^$s0u`Ts zaC&bb%-O-!;!(OoTU1n30A8h5+d}Lug3+O*)S>1zcVk*uhPUUzi_X}#X>mv@l~-is zs2jO|%_3P7kSHXTG|!!56Z_EpOgp=u_&Fq%fy2*Vgo|CUV{R}#y^Egq1*e51PL2tg zjn@_cNfLz?^G@dOL=D&xrG_4(f>I>|T7A_;$K4T&uF3l+tlIX*s>!wUJDDmD?`n7x zUgw0s3#IcdG}@>)F_HBiOU-Gfj)nejZ3}9Sv(@%tqG8T#b`+z#d31qT{Okfx-c_*0 zY*}f#Yg&JUL3{oo$h=V-V?!_z1xT!@yyVqUf`L>41U1H4jb-)+U4IR5drT8a4}JZZ zk<)ae!uFTzrnk~*g#v-SLtj}N^pQV*u4!v)OVS838|h?u80tu2zKC_O_Xmh8o$s`O zNO8ThXU}SB-5vRnessC}S4ZgRv|8{QN++cksKnOX!a>RJRzNo9j669icWbr$$9Zb$6TC9a~qC3iGU#K9`TX z)0BSYTOIEA)t*}A;3i~r{P^*!S92YYTyqi%EDj3`t1nZo+hH2JdHX&<)`#tHzEFK7 z1QQbz2jAYmp+2xmC%k)9Pg67O!`ZLd7w^3V`WZEAlbAPOVCS{Kv}gOfO|qu(0ThR? ztBjMaYv6yBj{8eY@dz65JI}1$7>t{BW>~OEx$de?%wtW9URyIdTV=PI+JN(gQEOIU zuIAJv|GaGP!t$RSf!t zdhaR~81MXzTK(_R zrhihUe^MpJr90W~9Zv7x{qcuCr_>a4xHN{gpoeZ~d6u?(6+@x4vupka^pj?bh7kZo zUS=UdZn6Jj>Uo%2eP%WAyi^spNpYpVCVUBqEv3@UxEMk|$U94>Ees6gxe$3JbXTuUbIIFHOk);KUmt0?V{~ zv;0Hbq}$giKS6GK0_tTFZs0tNb7>s|$Mc;NuJA?;)6JAKyI)lHV9v8t-EWQkp>Ulr zwU*e&Ag7qRm;>R^F1zKzpz&s$^gfHRVPgnsj%2r8LcJL@&bv8`OeLbA)&)wh)+y{= zZHtQBO6AK3cL&BH`Jwi;I4rYy9JT6|d1E#7^vGy%PLPn?i9$&I3LmrVI=#>s1g)}V z7QLHpIB;PtuzsD!u%OK6b@)1VtFYB><`a3im)lDzIYj{X+tY(pLcfgjxNN{C{n3%NlZ7ou(Jz@RH; zboQ)s1g9%om8N77>(oSBnoQ7fw6Y@LaHPuTQ_odaOs&|dXOjlSmngk0-%2+0v;>Cp z5WcgVZ0E^e*?xwr*zoz~^P7dTbK%@bsrMXo~`rU5pstFmwJE z=iN&L9gh`{zDxBI^9-Y1{TxYjSRi?7tqiyw$>aS|jUB@-#ntHvd}JD~x8VEB@||Pr zTK8PtoVi0)HbGLi8(`Fk-}-}TkPMm9{pA6k&BS`=sa17(V3F?X1 z{cT4h6Xm^uivhNTsfLr2Q|)`UA7_vzR=A{4!3ijC#dXUkG1j^DhY*1WqT^y*af((U zmDL1^Rf$j~FCIK7K3$s`?2K&}pyhv$Is#hjiwuBIplw0FjzdN*#T^q*bUOz{>Lj=& zr4`e`86Q_!F7Y*J)!m1a(t@Zpf6DW2$PxaZ~`a@8jDTls76r${al5SGv@T(rx#yA{x%I~(!EIh2-R zEGa9l=`Z!BFr*+>s2rgMg=HIs_= z?Q=M^c^FxALU^;XvY2dE7%ktd zPv*`E30?jr1Ei<;-8s)^gRZDXiT;rPj6=%xou;-z)}#5h2g`G*%hU>QXb}ycqAf13 zas0zcI-qv9loWSmujQJ{6Y88nFG*_vnfntFs2X$m^d-BN_ z3>(Ar;y#O#$G;A9b z+gD8YLWd)(Vx{GBAb$%!p{nSu?Y9~^1F8G2vfud+h}UDbwMkbpB%VTOWq?q z+pUi2o#c#jJDLgpI5vWFCbI6$)a4|RfftqL&3tJHhgfPZ@ibzP{tgvvxj3S>XlI|= ze+AL}Db;%LdmlA-6vWV)j)<0nfz&$L{caL%Z%+SKn>S2YKZ>g}81TYOxm;cB?s+M+ zSlk4UPKsSJ9yS()ch%3jTR;)jx+?i?#|OxyR=LyZps8w6zmxGG67JE_eZtP~0*ua>O|q z7C*tLXer+HrVL#P#O#AOS31+^l2BeaMrC9>vfnGE0N_BpFs>aF2dcB7pmI7l?~O~` zIihw;6OwpaX;3I-T`Lcc;nTQ$dsOOvDxasijh3iciTcC$D0fhPx6%J~nj~i1^KNczo$>h>{Qv^)=71?bkAw?{W zhZoiy+8I3cE!Eo-Mn&))VgDhc!)gj1$U%thlFC<>qHxw`r!T%3elfVOl)K0D=79qT zpw}%A8ZEc>22=%4W8Bri4sHGVw{N$Gammm=Z1)7ZxwU`B1b*?bOcEpYJT8De~WQ`quG-~y3&;SyC0MIW}C-Y)qi#I zr(DbplMb{mJ1Y!Nu=%DA>Gt*G@|kr3H()6ky{V%quyw60UX~P=yqEvv{O#imz7{-9 z6OiG}QyG^bh+%H%mjmJYXP{%%bE$uW#*MLYqzT9q`zwuQtfoZ81E`Aw;w1jN7}2?C z!^Nex&ZV8HGx*zf$?flVWX#7BqG3#6Wn^wtKAO1JB(|mN}#{t|6 zK2IN1%2OPD*of(gc4MdCTO`@U60vt8lF6&GaAYUR`2xE&c|`scZ&}SF!p)M=n1dh6 zV>E|vtDqQa-k`o+rMAYhAWlyt>J1GU%BKsp_b20@Mp=$U)^LszeG$Qfgs`=*d;PD> z75ES3c+EA^9x+?Q*egzK`84drtZG;_WZUIkQDJaoKzvm}WwBkgWkKui#V<;Nmkv$4 zNTyW>zwMu_GDLr-96ZvEr$XOcTt#P`ykdkw0ote(ns?JNIV|VQYPwAG725wdx9m?I z>h_efGDUBCR%1`*^?h*cjKzqnSZNBE4f+M~;xr-iB7XB9v?8n_JGhts;d6ipv_jP2 zj?P3#Nc6dHpi5MSoB8nZV?Ix9JzNRbQ&m+xckW!l`n5&Ne((*Yj69C5`B{f}^w5D$ zs;IgSjL7q&fxh*9ob-}}JdBWUf)w<>zbYuK`>qCejQX}M3IKGjvyHRVe%XlrRH zvhyC*_IEn6Vn*I}kc;Q%>CQ|_Nl5{Mm3^|w*nnDW2l3fWJ*(~M^<%sc$U<#iZiiCPC~@{bUj{JL|EtbBdkBCCY;%v3;5t#K=(bDZuh_6lWZU`8&r%+iw?Rtg ztE!mxp=*K`wb2k7-1rD{7N%QGu?crw*~%^$f!`7no&Ld$m;7VJs*tOfa30qY~BM zo%KEDop$=Vx=#b>#}6@-+MA^$C##O6Psy6y+xlGy6un{mnU0Q8p1e~~Lanf>x_Ux} zgKkNkEmWt%~o9k}3@+=7!!3T@$mtd|7gc8 z?Vjz_K7`=lRm&ZbTWE>j3=8l`)!E8kBk8H8sh?gQ8#d#jnsTGLn)6 zqCCjUFMu*MwGFarDQz*Zl7aJ<-WN<|5n&4fxpEH2Q#R(vP$htv!|)si`rdTt`8{M7 zghj^Z3H2lD!H{H67tm!to&{77aG06A&c#Db~U_=KnQB)ceJCS?E+q>jzf)YLfe@CNf7)OEWApBiZ8&Bk>;Z;3y%FmvTSd{i~Ld51L|fVU6CemNot!oPQ@}co)&H zFPVhQb`?nD8#t1prndHbkJw<3v1Yh^O1~&-e)i&b5ZBuFjoniUOO#nnqASuO&Ub@< zMl*>ToSA(bALxb_1k-&NFT=wXZ@*lOFT}zK&?+*Gv)uKMoS1K(iZO$1#h9r@Kv62Ew1di2kIMxUU zm)AEFYimeD_kGb*$YeDr3Qa%2QdNBLih{OzA2Qb%!y^*`-d&=~>uFfsG|U+}r6dn1 zSVbC4*073PDZC9D6xMZ<(NI>SaigrQ(9q4?{o)=yxHb->}(`D zRHl*HYykKvwg}C>j13ZE2X2lhbOPiK;DL5ux3W=sPMN)gW+Bo9-fS6imEQ?t2f455 zRrYAM1h=pWGaaWktH}v2KmbH>_tmX4NJm5w$>JGE7qOgr$cJ9<-!X(4j%BXSfT&?x zc=*e9cX2WkS9k9%j&a}W)%=eC4@STJ2X>iAvqQ6GS77|bL5)BZO8~=I^K=kTkgUf= zae56ckauf^g&CnL)ZglyDHHda%AlEBU=^pqb7ku74+m3EOOALB$2Lf!+Xq{%ABw4+ zy=|J##KF2&L|QPyZV_5+MZ0@_1}>@Pse~obt4>ebmhg3Ix3~8sIBy3yFsc}NaaX-sYy{<93BkgZaCyGaRI{bY6Mh28<)S@H4dvKj8taHC5Iy=+PIpbHK!449^1+@eM&q zX=PTIW||-YgyuPvbi&rKGBH3?W~WXVr|;4F7h{`CB!J8?GW`>ji6fpLiLuYgWgYln zsoH31TMrpA()ebtAAQMd9QLhS#M`%uk(YBN#0R|62XRdHPuM})vB}^lXfp}4kj2VW z{><~Jw^|F!RsTllP+mnN$yhSJ7gyZy+y!L^=KQq~1GtW+?3%^&SzbHO9`?=(wa(aJ zHSt*~MUn&#U4Ow_b*mHS)%I@JsLqO5-2hJIj)-6>3+xu0o>u8P+vlGK_1*U$3J(=& z=zqeN?9cpPNUF+rAreWZC;XW#Ar+f+3kVS@$Dbd={3fvYPsaLvT}XwH>j)nEd&B)c zQg~kx9r!aZ+mD1(e_kt)(BR-8@}>xqj?3`TEg5m#b?lt&X5M%0^*(@ZE$|#&-GRwX z#-4ARn^|vtLR`}L8xU`inb4l{6F@Jhmzb4Lem6v7Px<+y^ZY7>d22QX5~}FdtqF@? zUw=rof^IeouWggq!(!g+F0euY*Ye1WRT;{wjeN4UwB!>Ka#36NY`E0^d3yShR*A}7 zlcdn=$AJ5PxiimZAk372;pI+9D2^%{&<&p9@Jd8L!2M~5))vAqw#V79TNL_x_w*(` zc(AXoq1D1BvtBX9fG)`?fDOZ4b>f)HKRo>1x|E@C1tI@fm0v1Zd6dzQIcvcGWx(cp zg364Axxz!*p(}p-{i--G^Uhl_qIu6Y7bPT=@|U%;t^jFctIavC#C8v%tfSBYK-;t& zQ*#&wBa0pL)h}8H$zZKeTUlKV-T3P+mgd96cMo=V_FGvk z|G$9yzro1<0XYB9tK$y*4~EMBPhLH$Zw$vaE!-a8UY^TSAczEZQosKzH+L64U6G+4 kUPXjFc;r0!DcMD#fvJ8rT7l-z_$QsRg4&6cW5&Py3*i`QU;qFB literal 0 HcmV?d00001 diff --git a/third_party/README.md b/third_party/README.md index f6ab947..5c3f21c 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -1,6 +1,37 @@ # Third-Party Dependencies -This directory contains third-party source that is vendored into BusyMax. +This directory contains third-party source vendored into BusyMax. Direct Dart +dependencies with security or licensing significance are also listed here. + +## xml + +- Package: `xml` +- Pinned version: `6.6.1` +- Source: https://github.com/renggli/dart-xml +- License: MIT +- Purpose: namespace-aware WebDAV/CalDAV XML parsing +- Native/transitive review: pure Dart; no transitive native dependency is + introduced by this direct dependency. + +BusyMax applies its own parser limits and rejects DTD and entity declarations. +DAV properties are identified by namespace URI and local name, not by prefix. + +## posix + +- Package: `posix` +- Pinned lockfile version: `6.5.0` +- Source: https://github.com/onepub-dev/dart_posix +- License: MIT +- Purpose: apply restrictive `0700` directory and `0600` file modes to the + strict-Snap portal-encrypted credential store +- Native/transitive review: Dart FFI calls the platform C library; BusyMax uses + only `chmod`, only on Linux/macOS, and maps failures to the typed + secret-store-unavailable state. + +The encrypted credential store repairs inherited permissions before reading +and creates replacement files atomically. See +[`docs/icalendar_data_model.md`](../docs/icalendar_data_model.md) for the +CalDAV and iCalendar dependency decision. ## xdg_status_notifier_item @@ -34,11 +65,8 @@ BusyMax-specific patches currently include: - Adding `ItemIsMenu`, custom menu path, object path accessors, and diagnostic logging hooks used by BusyMax tray tests and runtime diagnostics. -Plan: +### Maintenance -- Keep the vendored package scoped to tray support only. -- Do not commit build artifacts, generated outputs, or dependency caches under - `third_party`. - Upstream the StatusNotifierItem/DBusMenu fixes where practical, or replace this vendored copy with a maintained pub.dev release once the required behavior is available. diff --git a/tools/README.md b/tools/README.md index 4846128..2f7b537 100644 --- a/tools/README.md +++ b/tools/README.md @@ -5,4 +5,5 @@ Project maintenance scripts live here. - `build_install_snap_local.sh` builds and installs a local Snap package. See the [Snap build and beta release guide](../docs/beta_snap_release.md) before using its scaffold workflow or publishing an artifact. -- `google_tasks_discovery/fetch_tasks_discovery.dart` refreshes the cached Google Tasks v1 discovery document and checks its locked revision. +- `google_tasks_discovery/fetch_tasks_discovery.dart` refreshes the cached + Google Tasks v1 discovery document and checks its locked revision. From ddeb3de4664fb440e802b4331fe83a69e2235b6c Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 10 Aug 2026 16:51:56 -0700 Subject: [PATCH 5/5] Refine DAV collection settings --- lib/l10n/app_ar.arb | 6 +- lib/l10n/app_de.arb | 6 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 6 +- lib/l10n/app_et.arb | 6 +- lib/l10n/app_fa.arb | 6 +- lib/l10n/app_fi.arb | 6 +- lib/l10n/app_fr.arb | 6 +- lib/l10n/app_hi.arb | 6 +- lib/l10n/app_it.arb | 6 +- lib/l10n/app_ja.arb | 6 +- lib/l10n/app_ko.arb | 6 +- lib/l10n/app_pt.arb | 6 +- lib/l10n/app_ru.arb | 6 +- lib/l10n/app_vi.arb | 6 +- lib/l10n/app_zh.arb | 6 +- lib/l10n/app_zh_Hans.arb | 6 +- lib/l10n/app_zh_Hant.arb | 6 +- lib/l10n/generated/app_localizations.dart | 6 +- lib/l10n/generated/app_localizations_ar.dart | 6 +- lib/l10n/generated/app_localizations_de.dart | 6 +- lib/l10n/generated/app_localizations_en.dart | 6 +- lib/l10n/generated/app_localizations_es.dart | 6 +- lib/l10n/generated/app_localizations_et.dart | 6 +- lib/l10n/generated/app_localizations_fa.dart | 6 +- lib/l10n/generated/app_localizations_fi.dart | 6 +- lib/l10n/generated/app_localizations_fr.dart | 6 +- lib/l10n/generated/app_localizations_hi.dart | 6 +- lib/l10n/generated/app_localizations_it.dart | 6 +- lib/l10n/generated/app_localizations_ja.dart | 6 +- lib/l10n/generated/app_localizations_ko.dart | 6 +- lib/l10n/generated/app_localizations_pt.dart | 6 +- lib/l10n/generated/app_localizations_ru.dart | 6 +- lib/l10n/generated/app_localizations_vi.dart | 6 +- lib/l10n/generated/app_localizations_zh.dart | 18 +- .../presentation/settings_screen.dart | 174 ++++++++++++------ .../presentation/settings_screen_test.dart | 148 ++++++++++++++- 37 files changed, 380 insertions(+), 164 deletions(-) diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 3a0ddd9..c1ac539 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index fbf42e7..cab9c91 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -454,10 +454,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -486,7 +486,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2d12274..51cd328 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -541,10 +541,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -559,7 +559,7 @@ "davLastSuccessfulSync": "Last successful sync: {time}", "@davLastSuccessfulSync": {"placeholders": {"time": {"type": "String"}}}, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": {"placeholders": {"host": {"type": "String"}}}, "collectionSupportsEvents": "Event calendar", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1c24b87..8c2ee64 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -454,10 +454,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -486,7 +486,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb index ebd1e44..b191fb7 100644 --- a/lib/l10n/app_et.arb +++ b/lib/l10n/app_et.arb @@ -569,10 +569,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -601,7 +601,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index d98cd8b..131624f 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -487,7 +487,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index c4546a6..f3d749e 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8ffb024..b5a5780 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -454,10 +454,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -486,7 +486,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 8d9dcbb..a041bb1 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 3855c4b..9193832 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index ad6f633..9908fe8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index a60b91e..b41811c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fa0d066..0626073 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 23369f2..ae3c1ab 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index fb69297..8592025 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6f2ed98..bf244e9 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index fd64f10..060a86d 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 0bf004d..b798426 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -398,10 +398,10 @@ "davTemporarilyUnavailable": "This account is temporarily unavailable.", "davPermissionChanged": "Server permissions changed. Pending edits are paused.", "davUnsupportedServer": "This server or provider profile is not supported.", - "collectionSettings": "Collections", + "collectionSettings": "Calendars and task lists", "calendarContent": "Calendar events", "taskContent": "Tasks", - "readOnlySharedCollection": "Read-only or shared", + "readOnlySharedCollection": "Read-only", "pendingLocally": "Pending locally", "conflictBlocked": "Blocked by conflict", "authenticationBlocked": "Blocked until reconnect", @@ -423,7 +423,7 @@ } }, "davNeverSynced": "Not synchronized yet", - "refreshCollections": "Refresh collections", + "refreshCollections": "Refresh calendars and task lists", "nextcloudServerHost": "Server: {host}", "@nextcloudServerHost": { "placeholders": { diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 01e9b62..67b9d2e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -3063,7 +3063,7 @@ abstract class AppLocalizations { /// No description provided for @collectionSettings. /// /// In en, this message translates to: - /// **'Collections'** + /// **'Calendars and task lists'** String get collectionSettings; /// No description provided for @calendarContent. @@ -3081,7 +3081,7 @@ abstract class AppLocalizations { /// No description provided for @readOnlySharedCollection. /// /// In en, this message translates to: - /// **'Read-only or shared'** + /// **'Read-only'** String get readOnlySharedCollection; /// No description provided for @pendingLocally. @@ -3165,7 +3165,7 @@ abstract class AppLocalizations { /// No description provided for @refreshCollections. /// /// In en, this message translates to: - /// **'Refresh collections'** + /// **'Refresh calendars and task lists'** String get refreshCollections; /// No description provided for @nextcloudServerHost. diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart index e9b5c7f..fbb898e 100644 --- a/lib/l10n/generated/app_localizations_ar.dart +++ b/lib/l10n/generated/app_localizations_ar.dart @@ -1668,7 +1668,7 @@ class AppLocalizationsAr extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1677,7 +1677,7 @@ class AppLocalizationsAr extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1721,7 +1721,7 @@ class AppLocalizationsAr extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 871492a..b2bc599 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1665,7 +1665,7 @@ class AppLocalizationsDe extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1674,7 +1674,7 @@ class AppLocalizationsDe extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1718,7 +1718,7 @@ class AppLocalizationsDe extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 47c31b5..f5d7745 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -1649,7 +1649,7 @@ class AppLocalizationsEn extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1658,7 +1658,7 @@ class AppLocalizationsEn extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1702,7 +1702,7 @@ class AppLocalizationsEn extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index ac1dd30..2e88b64 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1666,7 +1666,7 @@ class AppLocalizationsEs extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1675,7 +1675,7 @@ class AppLocalizationsEs extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1719,7 +1719,7 @@ class AppLocalizationsEs extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart index 2fbac7f..06e218b 100644 --- a/lib/l10n/generated/app_localizations_et.dart +++ b/lib/l10n/generated/app_localizations_et.dart @@ -1656,7 +1656,7 @@ class AppLocalizationsEt extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1665,7 +1665,7 @@ class AppLocalizationsEt extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1709,7 +1709,7 @@ class AppLocalizationsEt extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart index d264e4e..48907da 100644 --- a/lib/l10n/generated/app_localizations_fa.dart +++ b/lib/l10n/generated/app_localizations_fa.dart @@ -1691,7 +1691,7 @@ class AppLocalizationsFa extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1700,7 +1700,7 @@ class AppLocalizationsFa extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1744,7 +1744,7 @@ class AppLocalizationsFa extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart index 59d2030..10f91fb 100644 --- a/lib/l10n/generated/app_localizations_fi.dart +++ b/lib/l10n/generated/app_localizations_fi.dart @@ -1661,7 +1661,7 @@ class AppLocalizationsFi extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1670,7 +1670,7 @@ class AppLocalizationsFi extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1714,7 +1714,7 @@ class AppLocalizationsFi extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index dfa569a..cf80e22 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1664,7 +1664,7 @@ class AppLocalizationsFr extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1673,7 +1673,7 @@ class AppLocalizationsFr extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1717,7 +1717,7 @@ class AppLocalizationsFr extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart index a7c2c19..2a4c6f7 100644 --- a/lib/l10n/generated/app_localizations_hi.dart +++ b/lib/l10n/generated/app_localizations_hi.dart @@ -1655,7 +1655,7 @@ class AppLocalizationsHi extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1664,7 +1664,7 @@ class AppLocalizationsHi extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1708,7 +1708,7 @@ class AppLocalizationsHi extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 7918efe..4ab26e1 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1667,7 +1667,7 @@ class AppLocalizationsIt extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1676,7 +1676,7 @@ class AppLocalizationsIt extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1720,7 +1720,7 @@ class AppLocalizationsIt extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart index e97fc84..7dafd84 100644 --- a/lib/l10n/generated/app_localizations_ja.dart +++ b/lib/l10n/generated/app_localizations_ja.dart @@ -1631,7 +1631,7 @@ class AppLocalizationsJa extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1640,7 +1640,7 @@ class AppLocalizationsJa extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1684,7 +1684,7 @@ class AppLocalizationsJa extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart index 97ddc6f..eab5907 100644 --- a/lib/l10n/generated/app_localizations_ko.dart +++ b/lib/l10n/generated/app_localizations_ko.dart @@ -1631,7 +1631,7 @@ class AppLocalizationsKo extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1640,7 +1640,7 @@ class AppLocalizationsKo extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1684,7 +1684,7 @@ class AppLocalizationsKo extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index ab09256..08918c0 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1665,7 +1665,7 @@ class AppLocalizationsPt extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1674,7 +1674,7 @@ class AppLocalizationsPt extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1718,7 +1718,7 @@ class AppLocalizationsPt extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index 371e8b2..12b1ee3 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -1668,7 +1668,7 @@ class AppLocalizationsRu extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1677,7 +1677,7 @@ class AppLocalizationsRu extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1721,7 +1721,7 @@ class AppLocalizationsRu extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart index dc2e534..c37e912 100644 --- a/lib/l10n/generated/app_localizations_vi.dart +++ b/lib/l10n/generated/app_localizations_vi.dart @@ -1656,7 +1656,7 @@ class AppLocalizationsVi extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1665,7 +1665,7 @@ class AppLocalizationsVi extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1709,7 +1709,7 @@ class AppLocalizationsVi extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 29d54a0..d23be00 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -1623,7 +1623,7 @@ class AppLocalizationsZh extends AppLocalizations { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -1632,7 +1632,7 @@ class AppLocalizationsZh extends AppLocalizations { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -1676,7 +1676,7 @@ class AppLocalizationsZh extends AppLocalizations { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { @@ -3367,7 +3367,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -3376,7 +3376,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -3420,7 +3420,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { @@ -5112,7 +5112,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { 'This server or provider profile is not supported.'; @override - String get collectionSettings => 'Collections'; + String get collectionSettings => 'Calendars and task lists'; @override String get calendarContent => 'Calendar events'; @@ -5121,7 +5121,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { String get taskContent => 'Tasks'; @override - String get readOnlySharedCollection => 'Read-only or shared'; + String get readOnlySharedCollection => 'Read-only'; @override String get pendingLocally => 'Pending locally'; @@ -5165,7 +5165,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { String get davNeverSynced => 'Not synchronized yet'; @override - String get refreshCollections => 'Refresh collections'; + String get refreshCollections => 'Refresh calendars and task lists'; @override String nextcloudServerHost(String host) { diff --git a/lib/src/features/settings/presentation/settings_screen.dart b/lib/src/features/settings/presentation/settings_screen.dart index 0d02106..cd5dd65 100644 --- a/lib/src/features/settings/presentation/settings_screen.dart +++ b/lib/src/features/settings/presentation/settings_screen.dart @@ -1097,10 +1097,11 @@ class _AccountManagementSection extends StatelessWidget { if (account.provider == BusyProvider.appleICloud || account.provider == BusyProvider.nextcloud) _DavCollectionsCard( - account: account, collections: [ for (final collection in davCollections) - if (collection.accountId == account.id) collection, + if (collection.accountId == account.id && + (collection.supportsEvents || collection.supportsTasks)) + collection, ], onEventsSelected: onEventsSelected, onTasksSelected: onTasksSelected, @@ -1218,13 +1219,11 @@ class _AccountManagementCard extends StatelessWidget { class _DavCollectionsCard extends StatelessWidget { const _DavCollectionsCard({ - required this.account, required this.collections, required this.onEventsSelected, required this.onTasksSelected, }); - final AccountEntity account; final List collections; final void Function(DavCollectionSettingsEntity collection, bool selected) onEventsSelected; @@ -1233,60 +1232,133 @@ class _DavCollectionsCard extends StatelessWidget { @override Widget build(BuildContext context) { + if (collections.isEmpty) { + return const SizedBox.shrink(); + } + final l10n = context.l10n; return BusyMaxGroupedList( title: l10n.collectionSettings, - description: account.displayLabel, filled: true, children: [ - for (final collection in collections) ...[ - BusyMaxActionRow( + for (final collection in collections) + _DavCollectionItem( key: ValueKey('dav-collection-${collection.id}'), - title: collection.name, - subtitle: _davCollectionSummary(context, collection), - leading: _DavCollectionColor(color: collection.color), + collection: collection, + onEventsSelected: onEventsSelected, + onTasksSelected: onTasksSelected, ), - if (collection.supportsEvents) - BusyMaxSwitchRow( - key: ValueKey('dav-events-toggle-${collection.id}'), - title: l10n.calendarContent, - subtitle: collection.readOnly ? l10n.readOnlyCalendar : null, - value: collection.eventsSelected, - onChanged: (selected) => onEventsSelected(collection, selected), - leading: const Icon(YaruIcons.calendar), + ], + ); + } +} + +class _DavCollectionItem extends StatelessWidget { + const _DavCollectionItem({ + super.key, + required this.collection, + required this.onEventsSelected, + required this.onTasksSelected, + }); + + final DavCollectionSettingsEntity collection; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onEventsSelected; + final void Function(DavCollectionSettingsEntity collection, bool selected) + onTasksSelected; + + @override + Widget build(BuildContext context) { + final supportsEvents = collection.supportsEvents; + final supportsTasks = collection.supportsTasks; + final details = _davCollectionDetails(context, collection); + final indicator = _DavCollectionIndicator(color: collection.color); + + if (supportsEvents && !supportsTasks) { + return BusyMaxSwitchRow( + key: ValueKey('dav-events-toggle-${collection.id}'), + title: collection.name, + subtitle: details, + value: collection.eventsSelected, + onChanged: (selected) => onEventsSelected(collection, selected), + leading: indicator, + ); + } + + if (supportsTasks && !supportsEvents) { + return BusyMaxSwitchRow( + key: ValueKey('dav-tasks-toggle-${collection.id}'), + title: collection.name, + subtitle: details, + value: collection.tasksSelected, + onChanged: (selected) => onTasksSelected(collection, selected), + leading: indicator, + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + BusyMaxActionRow( + title: collection.name, + subtitle: details, + leading: indicator, + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: BusyMaxSpacing.xxl), + child: DecoratedBox( + decoration: BoxDecoration( + border: BorderDirectional( + start: BorderSide( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), ), - if (collection.supportsTasks) - BusyMaxSwitchRow( - key: ValueKey('dav-tasks-toggle-${collection.id}'), - title: l10n.taskContent, - subtitle: collection.readOnly - ? l10n.readOnlySharedCollection - : null, - value: collection.tasksSelected, - onChanged: (selected) => onTasksSelected(collection, selected), - leading: const Icon(YaruIcons.checkmark), + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: BusyMaxSpacing.xs, + ), + child: Column( + children: [ + BusyMaxSwitchRow( + key: ValueKey('dav-events-toggle-${collection.id}'), + title: context.l10n.calendarContent, + value: collection.eventsSelected, + onChanged: (selected) => + onEventsSelected(collection, selected), + leading: const Icon(YaruIcons.calendar), + ), + Divider( + height: 1, + thickness: 1, + color: Theme.of(context).colorScheme.outlineVariant, + ), + BusyMaxSwitchRow( + key: ValueKey('dav-tasks-toggle-${collection.id}'), + title: context.l10n.taskContent, + value: collection.tasksSelected, + onChanged: (selected) => + onTasksSelected(collection, selected), + leading: const Icon(YaruIcons.checkmark), + ), + ], + ), ), - ], - if (collections.isEmpty) - BusyMaxActionRow( - title: l10n.collectionSettings, - subtitle: l10n.davNeverSynced, - leading: const Icon(YaruIcons.calendar), ), + ), ], ); } } -class _DavCollectionColor extends StatelessWidget { - const _DavCollectionColor({required this.color}); +class _DavCollectionIndicator extends StatelessWidget { + const _DavCollectionIndicator({required this.color}); final String? color; @override Widget build(BuildContext context) { - return Semantics( - label: context.l10n.calendar, + return ExcludeSemantics( child: Container( width: BusyMaxSizes.iconSm, height: BusyMaxSizes.iconSm, @@ -1484,7 +1556,7 @@ String _accountConnectionStateLabel( AccountConnectionState.signedOut => context.l10n.davSignedOut, }; -String _davCollectionSummary( +String _davCollectionDetails( BuildContext context, DavCollectionSettingsEntity collection, ) { @@ -1498,19 +1570,17 @@ String _davCollectionSummary( (false, true) => l10n.collectionSupportsTasks, _ => l10n.collectionSettings, }; - final access = collection.readOnly - ? l10n.readOnlyCalendar - : collection.shared - ? l10n.sharedCollection - : l10n.writableCollection; - final sync = collection.syncErrorCode != null - ? l10n.collectionSyncError(collection.syncErrorCode!) - : collection.lastSyncAtUtc == null - ? l10n.davNeverSynced - : l10n.collectionLastSynced( - _formatDavDateTime(context, collection.lastSyncAtUtc!), - ); - return '$support · $access\n$sync'; + final details = [support]; + if (collection.readOnly) { + details.add(l10n.readOnlySharedCollection); + } + if (collection.shared) { + details.add(l10n.sharedCollection); + } + if (collection.syncErrorCode case final errorCode?) { + details.add(l10n.collectionSyncError(errorCode)); + } + return details.join(' · '); } String _formatDavDateTime(BuildContext context, DateTime value) { diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index ce9d676..5f74d88 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -15,8 +15,10 @@ import 'package:busymax/src/core/secrets/secret_store.dart'; import 'package:busymax/src/dav/auth/dav_account_onboarding_service.dart'; import 'package:busymax/src/dav/auth/nextcloud_login_flow_v2.dart'; import 'package:busymax/src/dav/dav_errors.dart'; +import 'package:busymax/src/dav/storage/dav_settings_repository.dart'; import 'package:busymax/src/db/app_database.dart'; import 'package:busymax/src/features/accounts/data/accounts_repository.dart'; +import 'package:busymax/src/features/accounts/domain/account_connection_state.dart'; import 'package:busymax/src/features/auth/data/auth_repository.dart'; import 'package:busymax/src/features/settings/presentation/settings_screen.dart'; import 'package:busymax/src/features/sync/sync_auth_error.dart'; @@ -26,6 +28,7 @@ import 'package:busymax/src/platform/native_menu_service.dart'; import 'package:busymax/src/features/task_lists/data/task_lists_repository.dart'; import 'package:busymax/src/features/tasks/presentation/desktop_date_time_fields.dart'; import 'package:busymax/src/providers/busy_provider.dart'; +import 'package:busymax/src/providers/provider_capabilities.dart'; import 'package:busymax/l10n/generated/app_localizations.dart'; import 'package:drift/native.dart'; import 'package:http/http.dart' as http; @@ -369,6 +372,109 @@ void main() { expect(find.text('Add Microsoft account'), findsOneWidget); }); + testWidgets( + 'Settings presents DAV calendars and task lists as named controls', + (tester) async { + final container = _container( + selectedAccountId: _nextcloudAccount.id, + authRepository: _FakeAuthRepository(), + accounts: const [_nextcloudAccount], + davCollections: [ + _davCollection( + id: 'personal-calendar', + name: 'Personal', + supportsEvents: true, + color: '#3366cc', + ), + _davCollection( + id: 'task-list', + name: 'NCC Task List', + supportsTasks: true, + tasksSelected: false, + readOnly: true, + lastSyncAtUtc: DateTime.utc(2026, 8, 10), + syncErrorCode: 'CalDavUnavailable', + ), + ], + ); + addTearDown(container.dispose); + + await _pumpSettings( + tester, + container, + logicalSize: const Size(1000, 900), + ); + + expect(find.text('Calendars and task lists'), findsOneWidget); + expect(find.text('Collections'), findsNothing); + expect(find.text('Refresh calendars and task lists'), findsOneWidget); + + final calendar = find.byKey( + const ValueKey('dav-collection-personal-calendar'), + ); + final taskList = find.byKey(const ValueKey('dav-collection-task-list')); + expect( + find.descendant( + of: calendar, + matching: find.byType(YaruSwitchListTile), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: taskList, + matching: find.byType(YaruSwitchListTile), + ), + findsOneWidget, + ); + expect(find.text('Personal'), findsOneWidget); + expect(find.text('NCC Task List'), findsOneWidget); + expect( + find.text('Task list · Read-only · Sync issue: CalDavUnavailable'), + findsOneWidget, + ); + expect(find.textContaining('Last synchronized:'), findsNothing); + }, + ); + + testWidgets('Settings nests controls for combined DAV content', ( + tester, + ) async { + final container = _container( + selectedAccountId: _nextcloudAccount.id, + authRepository: _FakeAuthRepository(), + accounts: const [_nextcloudAccount], + davCollections: [ + _davCollection( + id: 'combined', + name: 'Team schedule', + supportsEvents: true, + supportsTasks: true, + shared: true, + ), + ], + ); + addTearDown(container.dispose); + + await _pumpSettings(tester, container, logicalSize: const Size(1000, 900)); + + final combined = find.byKey(const ValueKey('dav-collection-combined')); + expect(find.text('Team schedule'), findsOneWidget); + expect(find.text('Events and tasks · Shared'), findsOneWidget); + expect( + find.descendant(of: combined, matching: find.byType(YaruSwitchListTile)), + findsNWidgets(2), + ); + expect( + find.descendant(of: combined, matching: find.text('Calendar events')), + findsOneWidget, + ); + expect( + find.descendant(of: combined, matching: find.text('Tasks')), + findsOneWidget, + ); + }); + testWidgets('Settings sidebar separates settings pages', (tester) async { final container = _container( selectedAccountId: 'google:g', @@ -803,6 +909,7 @@ ProviderContainer _container({ String? activeAccountIdOverride = _useDefaultActiveAccountId, bool useFlutterHeader = false, DavAccountOnboardingService? davOnboardingService, + List davCollections = const [], }) { return ProviderContainer( overrides: [ @@ -819,7 +926,7 @@ ProviderContainer _container({ (ref) => Stream.value(accounts), ), davCollectionsStreamProvider.overrideWith( - (ref) => Stream.value(const []), + (ref) => Stream.value(davCollections), ), davConflictsStreamProvider.overrideWith((ref) => Stream.value(const [])), selectedAccountIdProvider.overrideWith((ref) => selectedAccountId), @@ -1032,6 +1139,45 @@ const _nextcloudAccount = AccountEntity( authState: accountAuthStateSignedIn, ); +DavCollectionSettingsEntity _davCollection({ + required String id, + required String name, + bool supportsEvents = false, + bool supportsTasks = false, + bool eventsSelected = true, + bool tasksSelected = true, + bool readOnly = false, + bool shared = false, + DateTime? lastSyncAtUtc, + String? syncErrorCode, + String? color, +}) { + return DavCollectionSettingsEntity( + id: id, + accountId: _nextcloudAccount.id, + provider: BusyProvider.nextcloud, + accountLabel: _nextcloudAccount.displayName!, + accountAuthority: _nextcloudAccount.authority, + connectionState: AccountConnectionState.connected, + name: name, + color: color, + readOnly: readOnly, + shared: shared, + supportsEvents: supportsEvents, + supportsTasks: supportsTasks, + eventsSelected: eventsSelected, + tasksSelected: tasksSelected, + lastSyncAtUtc: lastSyncAtUtc, + syncErrorCode: syncErrorCode, + capabilities: CollectionCapabilities( + canRead: true, + canWriteContent: !readOnly, + supportsEvents: supportsEvents, + supportsTasks: supportsTasks, + ), + ); +} + const _reconnectRequiredGoogleAccount = AccountEntity( id: 'google:g', provider: BusyProvider.google,