From f1398e6d4c52b10e29d13c03691e0735cc29a2a4 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Sun, 30 Jul 2023 23:51:00 +0200 Subject: [PATCH] feat: Migrate to go_router --- lib/core/bloc/bloc_refresh_listenable.dart | 20 + lib/core/database/tables/global_settings.dart | 4 +- .../database/tables/local_user_account.dart | 10 +- .../database/tables/local_user_app_state.dart | 2 +- lib/core/navigation/push_routes.dart | 70 +--- lib/features/app_drawer/view/app_drawer.dart | 14 +- .../cubit/document_details_cubit.dart | 8 - .../cubit/document_details_state.dart | 1 - .../view/pages/document_details_page.dart | 66 +-- .../widgets/archive_serial_number_field.dart | 2 +- .../widgets/document_overview_widget.dart | 18 +- .../cubit/document_edit_cubit.dart | 5 + .../cubit/document_edit_state.dart | 1 + .../view/document_edit_page.dart | 58 +-- .../view/document_search_bar.dart | 18 +- .../view/document_search_page.dart | 9 +- .../view/sliver_search_bar.dart | 4 +- .../document_upload_preparation_page.dart | 27 +- .../documents/view/pages/documents_page.dart | 26 +- .../widgets/items/document_detailed_item.dart | 98 +++-- .../widgets/search/document_filter_form.dart | 15 +- .../view/impl/add_storage_path_page.dart | 6 +- .../edit_label/view/impl/add_tag_page.dart | 6 +- .../view/impl/edit_correspondent_page.dart | 6 +- .../view/impl/edit_document_type_page.dart | 6 +- .../view/impl/edit_storage_path_page.dart | 5 +- .../edit_label/view/impl/edit_tag_page.dart | 3 +- lib/features/edit_label/view/label_form.dart | 3 +- lib/features/home/view/home_page.dart | 330 --------------- lib/features/home/view/home_route.dart | 204 --------- lib/features/home/view/home_shell_widget.dart | 208 ++++++++++ lib/features/home/view/model/api_version.dart | 4 +- .../view/scaffold_with_navigation_bar.dart | 168 ++++++++ lib/features/inbox/cubit/inbox_cubit.dart | 1 + lib/features/inbox/view/pages/inbox_page.dart | 2 +- .../inbox/view/widgets/inbox_item.dart | 14 +- .../view/widgets/fullscreen_tags_form.dart | 2 +- .../tags/view/widgets/tags_form_field.dart | 3 +- .../labels/view/pages/labels_page.dart | 331 ++++++--------- .../labels/view/widgets/label_item.dart | 3 +- lib/features/landing/view/landing_page.dart | 51 +++ .../view/linked_documents_page.dart | 9 +- .../login/cubit/authentication_cubit.dart | 18 +- .../login/cubit/authentication_state.dart | 8 +- lib/features/login/view/add_account_page.dart | 161 ++++++++ lib/features/login/view/login_page.dart | 193 +++------ .../view/saved_view_details_page.dart | 11 +- .../settings/view/manage_accounts_page.dart | 20 +- lib/features/settings/view/settings_page.dart | 10 - .../view/widgets/user_settings_builder.dart | 2 +- .../view/similar_documents_view.dart | 9 +- lib/main.dart | 389 +++++++++++------- lib/routes/document_details_route.dart | 51 --- lib/routes/navigation_keys.dart | 8 + lib/routes/routes.dart | 20 + .../typed/branches/documents_route.dart | 113 +++++ lib/routes/typed/branches/inbox_route.dart | 17 + lib/routes/typed/branches/labels_route.dart | 84 ++++ lib/routes/typed/branches/landing_route.dart | 38 ++ lib/routes/typed/branches/scanner_route.dart | 82 ++++ .../typed/shells/provider_shell_route.dart | 72 ++++ .../typed/shells/scaffold_shell_route.dart | 29 ++ lib/routes/typed/top_level/login_route.dart | 30 ++ .../typed/top_level/settings_route.dart | 17 + .../top_level/switching_accounts_route.dart | 18 + .../top_level/verify_identity_route.dart | 19 + .../paperless_server_message_exception.dart | 4 +- .../models/labels/correspondent_model.dart | 74 ---- .../models/labels/document_type_model.dart | 59 --- .../lib/src/models/labels/label_model.dart | 284 ++++++++++++- .../src/models/labels/storage_path_model.dart | 71 ---- .../lib/src/models/labels/tag_model.dart | 91 ---- .../paperless_api/lib/src/models/models.dart | 4 - .../user_permission_extension.dart | 6 + .../labels_api/paperless_labels_api.dart | 6 +- .../labels_api/paperless_labels_api_impl.dart | 5 +- pubspec.lock | 96 +++-- pubspec.yaml | 2 + 78 files changed, 2206 insertions(+), 1756 deletions(-) create mode 100644 lib/core/bloc/bloc_refresh_listenable.dart delete mode 100644 lib/features/home/view/home_page.dart delete mode 100644 lib/features/home/view/home_route.dart create mode 100644 lib/features/home/view/home_shell_widget.dart create mode 100644 lib/features/home/view/scaffold_with_navigation_bar.dart create mode 100644 lib/features/landing/view/landing_page.dart create mode 100644 lib/features/login/view/add_account_page.dart delete mode 100644 lib/routes/document_details_route.dart create mode 100644 lib/routes/navigation_keys.dart create mode 100644 lib/routes/routes.dart create mode 100644 lib/routes/typed/branches/documents_route.dart create mode 100644 lib/routes/typed/branches/inbox_route.dart create mode 100644 lib/routes/typed/branches/labels_route.dart create mode 100644 lib/routes/typed/branches/landing_route.dart create mode 100644 lib/routes/typed/branches/scanner_route.dart create mode 100644 lib/routes/typed/shells/provider_shell_route.dart create mode 100644 lib/routes/typed/shells/scaffold_shell_route.dart create mode 100644 lib/routes/typed/top_level/login_route.dart create mode 100644 lib/routes/typed/top_level/settings_route.dart create mode 100644 lib/routes/typed/top_level/switching_accounts_route.dart create mode 100644 lib/routes/typed/top_level/verify_identity_route.dart delete mode 100644 packages/paperless_api/lib/src/models/labels/correspondent_model.dart delete mode 100644 packages/paperless_api/lib/src/models/labels/document_type_model.dart delete mode 100644 packages/paperless_api/lib/src/models/labels/storage_path_model.dart delete mode 100644 packages/paperless_api/lib/src/models/labels/tag_model.dart diff --git a/lib/core/bloc/bloc_refresh_listenable.dart b/lib/core/bloc/bloc_refresh_listenable.dart new file mode 100644 index 0000000..f7b8067 --- /dev/null +++ b/lib/core/bloc/bloc_refresh_listenable.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +class GoRouterRefreshStream extends ChangeNotifier { + GoRouterRefreshStream(Stream stream) { + notifyListeners(); + _subscription = stream.asBroadcastStream().listen( + (dynamic _) => notifyListeners(), + ); + } + + late final StreamSubscription _subscription; + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/lib/core/database/tables/global_settings.dart b/lib/core/database/tables/global_settings.dart index 6ee55c9..dda6961 100644 --- a/lib/core/database/tables/global_settings.dart +++ b/lib/core/database/tables/global_settings.dart @@ -21,7 +21,7 @@ class GlobalSettings with HiveObjectMixin { bool showOnboarding; @HiveField(4) - String? currentLoggedInUser; + String? loggedInUserId; @HiveField(5) FileDownloadType defaultDownloadType; @@ -37,7 +37,7 @@ class GlobalSettings with HiveObjectMixin { this.preferredThemeMode = ThemeMode.system, this.preferredColorSchemeOption = ColorSchemeOption.classic, this.showOnboarding = true, - this.currentLoggedInUser, + this.loggedInUserId, this.defaultDownloadType = FileDownloadType.alwaysAsk, this.defaultShareType = FileDownloadType.alwaysAsk, this.enforceSinglePagePdfUpload = false, diff --git a/lib/core/database/tables/local_user_account.dart b/lib/core/database/tables/local_user_account.dart index 64e6cc4..799ddb1 100644 --- a/lib/core/database/tables/local_user_account.dart +++ b/lib/core/database/tables/local_user_account.dart @@ -20,16 +20,16 @@ class LocalUserAccount extends HiveObject { @HiveField(7) UserModel paperlessUser; + @HiveField(8, defaultValue: 2) + int apiVersion; + LocalUserAccount({ required this.id, required this.serverUrl, required this.settings, required this.paperlessUser, + required this.apiVersion, }); - static LocalUserAccount get current => - Hive.box(HiveBoxes.localUserAccount).get( - Hive.box(HiveBoxes.globalSettings) - .getValue()! - .currentLoggedInUser)!; + bool get hasMultiUserSupport => apiVersion >= 3; } diff --git a/lib/core/database/tables/local_user_app_state.dart b/lib/core/database/tables/local_user_app_state.dart index 687eb3a..49812e2 100644 --- a/lib/core/database/tables/local_user_app_state.dart +++ b/lib/core/database/tables/local_user_app_state.dart @@ -43,7 +43,7 @@ class LocalUserAppState extends HiveObject { final currentLocalUserId = Hive.box(HiveBoxes.globalSettings) .getValue()! - .currentLoggedInUser!; + .loggedInUserId!; return Hive.box(HiveBoxes.localUserAppState) .get(currentLocalUserId)!; } diff --git a/lib/core/navigation/push_routes.dart b/lib/core/navigation/push_routes.dart index 82bf09a..77e5312 100644 --- a/lib/core/navigation/push_routes.dart +++ b/lib/core/navigation/push_routes.dart @@ -4,11 +4,13 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:go_router/go_router.dart'; import 'package:hive/hive.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; @@ -29,7 +31,6 @@ import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.da import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -import 'package:paperless_mobile/routes/document_details_route.dart'; import 'package:provider/provider.dart'; // These are convenience methods for nativating to views without having to pass providers around explicitly. @@ -38,59 +39,18 @@ import 'package:provider/provider.dart'; Future pushDocumentSearchPage(BuildContext context) { final currentUser = Hive.box(HiveBoxes.globalSettings) .getValue()! - .currentLoggedInUser; + .loggedInUserId; final userRepo = context.read(); return Navigator.of(context).push( MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: userRepo), - ], - builder: (context, _) { - return BlocProvider( - create: (context) => DocumentSearchCubit( - context.read(), - context.read(), - Hive.box(HiveBoxes.localUserAppState) - .get(currentUser)!, - ), - child: const DocumentSearchPage(), - ); - }, - ), - ), - ); -} - -Future pushDocumentDetailsRoute( - BuildContext context, { - required DocumentModel document, - bool isLabelClickable = true, - bool allowEdit = true, - String? titleAndContentQueryString, -}) { - return Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - if (context.read().hasMultiUserSupport) - Provider.value(value: context.read()), - ], - child: DocumentDetailsRoute( - document: document, - isLabelClickable: isLabelClickable, + builder: (_) => BlocProvider( + create: (context) => DocumentSearchCubit( + context.read(), + context.read(), + Hive.box(HiveBoxes.localUserAppState) + .get(currentUser)!, ), + child: const DocumentSearchPage(), ), ), ); @@ -106,7 +66,7 @@ Future pushSavedViewDetailsRoute( builder: (_) => MultiProvider( providers: [ Provider.value(value: apiVersion), - if (apiVersion.hasMultiUserSupport) + if (context.watch().hasMultiUserSupport) Provider.value(value: context.read()), Provider.value(value: context.read()), Provider.value(value: context.read()), @@ -147,8 +107,10 @@ Future pushAddSavedViewRoute(BuildContext context, ); } -Future pushLinkedDocumentsView(BuildContext context, - {required DocumentFilter filter}) { +Future pushLinkedDocumentsView( + BuildContext context, { + required DocumentFilter filter, +}) { return Navigator.push( context, MaterialPageRoute( @@ -161,7 +123,7 @@ Future pushLinkedDocumentsView(BuildContext context, Provider.value(value: context.read()), Provider.value(value: context.read()), Provider.value(value: context.read()), - if (context.read().hasMultiUserSupport) + if (context.watch().hasMultiUserSupport) Provider.value(value: context.read()), ], builder: (context, _) => BlocProvider( diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index 48adf98..627e893 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -9,6 +9,7 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -91,18 +92,7 @@ class AppDrawer extends StatelessWidget { title: Text( S.of(context)!.settings, ), - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value( - value: context.read()), - Provider.value(value: context.read()), - ], - child: const SettingsPage(), - ), - ), - ), + onTap: () => SettingsRoute().push(context), ), ], ), diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 3ad7007..94291ea 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -45,7 +45,6 @@ class DocumentDetailsCubit extends Cubit { ), ), ); - loadSuggestions(); loadMetaData(); } @@ -54,13 +53,6 @@ class DocumentDetailsCubit extends Cubit { _notifier.notifyDeleted(document); } - Future loadSuggestions() async { - final suggestions = await _api.findSuggestions(state.document); - if (!isClosed) { - emit(state.copyWith(suggestions: suggestions)); - } - } - Future loadMetaData() async { final metaData = await _api.getMetaData(state.document); if (!isClosed) { diff --git a/lib/features/document_details/cubit/document_details_state.dart b/lib/features/document_details/cubit/document_details_state.dart index 54a3751..6bcf0e7 100644 --- a/lib/features/document_details/cubit/document_details_state.dart +++ b/lib/features/document_details/cubit/document_details_state.dart @@ -7,7 +7,6 @@ class DocumentDetailsState with _$DocumentDetailsState { DocumentMetaData? metaData, @Default(false) bool isFullContentLoaded, String? fullContent, - FieldSuggestions? suggestions, @Default({}) Map correspondents, @Default({}) Map documentTypes, @Default({}) Map tags, diff --git a/lib/features/document_details/view/pages/document_details_page.dart b/lib/features/document_details/view/pages/document_details_page.dart index 8732da8..79216fb 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -14,8 +14,6 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart'; -import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; -import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; @@ -24,6 +22,7 @@ import 'package:paperless_mobile/features/similar_documents/cubit/similar_docume import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class DocumentDetailsPage extends StatefulWidget { final bool isLabelClickable; @@ -46,9 +45,9 @@ class _DocumentDetailsPageState extends State { @override Widget build(BuildContext context) { - final apiVersion = context.watch(); - - final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0); + final hasMultiUserSupport = + context.watch().hasMultiUserSupport; + final tabLength = 4 + (hasMultiUserSupport ? 1 : 0); return WillPopScope( onWillPop: () async { Navigator.of(context) @@ -171,7 +170,7 @@ class _DocumentDetailsPageState extends State { ), ), ), - if (apiVersion.hasMultiUserSupport) + if (hasMultiUserSupport) Tab( child: Text( "Permissions", @@ -259,7 +258,7 @@ class _DocumentDetailsPageState extends State { ), ], ), - if (apiVersion.hasMultiUserSupport) + if (hasMultiUserSupport) CustomScrollView( controller: _pagingScrollController, slivers: [ @@ -286,8 +285,10 @@ class _DocumentDetailsPageState extends State { } Widget _buildEditButton() { + final currentUser = context.watch(); + bool canEdit = context.watchInternetConnection && - LocalUserAccount.current.paperlessUser.canEditDocuments; + currentUser.paperlessUser.canEditDocuments; if (!canEdit) { return const SizedBox.shrink(); } @@ -302,7 +303,7 @@ class _DocumentDetailsPageState extends State { verticalOffset: 40, child: FloatingActionButton( child: const Icon(Icons.edit), - onPressed: () => _onEdit(state.document), + onPressed: () => EditDocumentRoute(state.document).push(context), ), ); }, @@ -316,9 +317,9 @@ class _DocumentDetailsPageState extends State { child: BlocBuilder( builder: (context, connectivityState) { final isConnected = connectivityState.isConnected; - - final canDelete = isConnected && - LocalUserAccount.current.paperlessUser.canDeleteDocuments; + final currentUser = context.watch(); + final canDelete = + isConnected && currentUser.paperlessUser.canDeleteDocuments; return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -360,47 +361,6 @@ class _DocumentDetailsPageState extends State { ); } - Future _onEdit(DocumentModel document) async { - { - final cubit = context.read(); - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: DocumentEditCubit( - context.read(), - context.read(), - context.read(), - document: document, - ), - ), - BlocProvider.value( - value: cubit, - ), - ], - child: BlocListener( - listenWhen: (previous, current) => - previous.document != current.document, - listener: (context, state) { - cubit.replace(state.document); - }, - child: BlocBuilder( - builder: (context, state) { - return DocumentEditPage( - suggestions: state.suggestions, - ); - }, - ), - ), - ), - maintainState: true, - ), - ); - } - } - void _onOpenFileInSystemViewer() async { final status = await context.read().openDocumentInSystemViewer(); diff --git a/lib/features/document_details/view/widgets/archive_serial_number_field.dart b/lib/features/document_details/view/widgets/archive_serial_number_field.dart index 1608010..ba2f6e2 100644 --- a/lib/features/document_details/view/widgets/archive_serial_number_field.dart +++ b/lib/features/document_details/view/widgets/archive_serial_number_field.dart @@ -47,7 +47,7 @@ class _ArchiveSerialNumberFieldState extends State { @override Widget build(BuildContext context) { final userCanEditDocument = - LocalUserAccount.current.paperlessUser.canEditDocuments; + context.watch().paperlessUser.canEditDocuments; return BlocListener( listenWhen: (previous, current) => previous.document.archiveSerialNumber != diff --git a/lib/features/document_details/view/widgets/document_overview_widget.dart b/lib/features/document_details/view/widgets/document_overview_widget.dart index 3e6804e..b19b49a 100644 --- a/lib/features/document_details/view/widgets/document_overview_widget.dart +++ b/lib/features/document_details/view/widgets/document_overview_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; @@ -47,7 +48,10 @@ class DocumentOverviewWidget extends StatelessWidget { label: S.of(context)!.createdAt, ).paddedOnly(bottom: itemSpacing), if (document.documentType != null && - LocalUserAccount.current.paperlessUser.canViewDocumentTypes) + context + .watch() + .paperlessUser + .canViewDocumentTypes) DetailsItem( label: S.of(context)!.documentType, content: LabelText( @@ -56,7 +60,10 @@ class DocumentOverviewWidget extends StatelessWidget { ), ).paddedOnly(bottom: itemSpacing), if (document.correspondent != null && - LocalUserAccount.current.paperlessUser.canViewCorrespondents) + context + .watch() + .paperlessUser + .canViewCorrespondents) DetailsItem( label: S.of(context)!.correspondent, content: LabelText( @@ -65,7 +72,10 @@ class DocumentOverviewWidget extends StatelessWidget { ), ).paddedOnly(bottom: itemSpacing), if (document.storagePath != null && - LocalUserAccount.current.paperlessUser.canViewStoragePaths) + context + .watch() + .paperlessUser + .canViewStoragePaths) DetailsItem( label: S.of(context)!.storagePath, content: LabelText( @@ -73,7 +83,7 @@ class DocumentOverviewWidget extends StatelessWidget { ), ).paddedOnly(bottom: itemSpacing), if (document.tags.isNotEmpty && - LocalUserAccount.current.paperlessUser.canViewTags) + context.watch().paperlessUser.canViewTags) DetailsItem( label: S.of(context)!.tags, content: Padding( diff --git a/lib/features/document_edit/cubit/document_edit_cubit.dart b/lib/features/document_edit/cubit/document_edit_cubit.dart index 966302f..84195dd 100644 --- a/lib/features/document_edit/cubit/document_edit_cubit.dart +++ b/lib/features/document_edit/cubit/document_edit_cubit.dart @@ -57,6 +57,11 @@ class DocumentEditCubit extends Cubit { } } + Future loadFieldSuggestions() async { + final suggestions = await _docsApi.findSuggestions(state.document); + emit(state.copyWith(suggestions: suggestions)); + } + void replace(DocumentModel document) { emit(state.copyWith(document: document)); } diff --git a/lib/features/document_edit/cubit/document_edit_state.dart b/lib/features/document_edit/cubit/document_edit_state.dart index 6504095..0f1bb39 100644 --- a/lib/features/document_edit/cubit/document_edit_state.dart +++ b/lib/features/document_edit/cubit/document_edit_state.dart @@ -4,6 +4,7 @@ part of 'document_edit_cubit.dart'; class DocumentEditState with _$DocumentEditState { const factory DocumentEditState({ required DocumentModel document, + FieldSuggestions? suggestions, @Default({}) Map correspondents, @Default({}) Map documentTypes, @Default({}) Map storagePaths, diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index ec83909..8cb014d 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -22,10 +22,8 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; class DocumentEditPage extends StatefulWidget { - final FieldSuggestions? suggestions; const DocumentEditPage({ Key? key, - required this.suggestions, }) : super(key: key); @override @@ -44,19 +42,12 @@ class _DocumentEditPageState extends State { final GlobalKey _formKey = GlobalKey(); bool _isSubmitLoading = false; - late final FieldSuggestions? _filteredSuggestions; - - @override - void initState() { - super.initState(); - _filteredSuggestions = widget.suggestions - ?.documentDifference(context.read().state.document); - } - @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + final filteredSuggestions = state.suggestions?.documentDifference( + context.read().state.document); return DefaultTabController( length: 2, child: Scaffold( @@ -94,8 +85,10 @@ class _DocumentEditPageState extends State { ListView( children: [ _buildTitleFormField(state.document.title).padded(), - _buildCreatedAtFormField(state.document.created) - .padded(), + _buildCreatedAtFormField( + state.document.created, + filteredSuggestions, + ).padded(), // Correspondent form field Column( children: [ @@ -123,15 +116,17 @@ class _DocumentEditPageState extends State { name: fkCorrespondent, prefixIcon: const Icon(Icons.person_outlined), allowSelectUnassigned: true, - canCreateNewLabel: LocalUserAccount.current - .paperlessUser.canCreateCorrespondents, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateCorrespondents, ), - if (_filteredSuggestions + if (filteredSuggestions ?.hasSuggestedCorrespondents ?? false) _buildSuggestionsSkeleton( suggestions: - _filteredSuggestions!.correspondents, + filteredSuggestions!.correspondents, itemBuilder: (context, itemData) => ActionChip( label: Text( @@ -160,8 +155,10 @@ class _DocumentEditPageState extends State { initialName: currentInput, ), ), - canCreateNewLabel: LocalUserAccount.current - .paperlessUser.canCreateDocumentTypes, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateDocumentTypes, addLabelText: S.of(context)!.addDocumentType, labelText: S.of(context)!.documentType, initialValue: @@ -175,12 +172,12 @@ class _DocumentEditPageState extends State { const Icon(Icons.description_outlined), allowSelectUnassigned: true, ), - if (_filteredSuggestions + if (filteredSuggestions ?.hasSuggestedDocumentTypes ?? false) _buildSuggestionsSkeleton( suggestions: - _filteredSuggestions!.documentTypes, + filteredSuggestions!.documentTypes, itemBuilder: (context, itemData) => ActionChip( label: Text( @@ -204,10 +201,12 @@ class _DocumentEditPageState extends State { RepositoryProvider.value( value: context.read(), child: AddStoragePathPage( - initalName: initialValue), + initialName: initialValue), ), - canCreateNewLabel: LocalUserAccount.current - .paperlessUser.canCreateStoragePaths, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateStoragePaths, addLabelText: S.of(context)!.addStoragePath, labelText: S.of(context)!.storagePath, options: state.storagePaths, @@ -232,14 +231,14 @@ class _DocumentEditPageState extends State { include: state.document.tags.toList(), ), ).padded(), - if (_filteredSuggestions?.tags + if (filteredSuggestions?.tags .toSet() .difference(state.document.tags.toSet()) .isNotEmpty ?? false) _buildSuggestionsSkeleton( suggestions: - (_filteredSuggestions?.tags.toSet() ?? {}), + (filteredSuggestions?.tags.toSet() ?? {}), itemBuilder: (context, itemData) { final tag = state.tags[itemData]!; return ActionChip( @@ -343,7 +342,8 @@ class _DocumentEditPageState extends State { ); } - Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) { + Widget _buildCreatedAtFormField( + DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -358,9 +358,9 @@ class _DocumentEditPageState extends State { format: DateFormat.yMMMMd(), initialEntryMode: DatePickerEntryMode.calendar, ), - if (_filteredSuggestions?.hasSuggestedDates ?? false) + if (filteredSuggestions?.hasSuggestedDates ?? false) _buildSuggestionsSkeleton( - suggestions: _filteredSuggestions!.dates, + suggestions: filteredSuggestions!.dates, itemBuilder: (context, itemData) => ActionChip( label: Text(DateFormat.yMMMd().format(itemData)), onPressed: () => _formKey.currentState?.fields[fkCreatedDate] diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index 7b4327b..08de60e 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -91,7 +91,7 @@ class _DocumentSearchBarState extends State { Provider.value(value: context.read()), Provider.value(value: context.read()), Provider.value(value: context.read()), - if (context.read().hasMultiUserSupport) + if (context.watch().hasMultiUserSupport) Provider.value(value: context.read()), ], child: Provider( @@ -99,7 +99,7 @@ class _DocumentSearchBarState extends State { context.read(), context.read(), Hive.box(HiveBoxes.localUserAppState) - .get(LocalUserAccount.current.id)!, + .get(context.watch().id)!, ), builder: (_, __) => const DocumentSearchPage(), ), @@ -112,19 +112,7 @@ class _DocumentSearchBarState extends State { IconButton _buildUserAvatar(BuildContext context) { return IconButton( padding: const EdgeInsets.all(6), - icon: GlobalSettingsBuilder( - builder: (context, settings) { - return ValueListenableBuilder( - valueListenable: - Hive.box(HiveBoxes.localUserAccount) - .listenable(), - builder: (context, box, _) { - final account = box.get(settings.currentLoggedInUser!)!; - return UserAvatar(account: account); - }, - ); - }, - ), + icon: UserAvatar(account: context.watch()), onPressed: () { final apiVersion = context.read(); showDialog( diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index f949891..8e43fa1 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; @@ -11,6 +12,7 @@ import 'package:paperless_mobile/features/document_search/view/remove_history_en import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class DocumentSearchPage extends StatefulWidget { const DocumentSearchPage({super.key}); @@ -218,11 +220,8 @@ class _DocumentSearchPageState extends State { hasLoaded: state.hasLoaded, enableHeroAnimation: false, onTap: (document) { - pushDocumentDetailsRoute( - context, - document: document, - isLabelClickable: false, - ); + DocumentDetailsRoute($extra: document, isLabelClickable: false) + .push(context); }, ) ], diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 67487b9..9f8610e 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -25,7 +25,7 @@ class SliverSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { - if (LocalUserAccount.current.paperlessUser.canViewDocuments) { + if (context.watch().paperlessUser.canViewDocuments) { return SliverAppBar( toolbarHeight: kToolbarHeight, flexibleSpace: Container( @@ -49,7 +49,7 @@ class SliverSearchBar extends StatelessWidget { Hive.box(HiveBoxes.localUserAccount) .listenable(), builder: (context, box, _) { - final account = box.get(settings.currentLoggedInUser!)!; + final account = box.get(settings.loggedInUserId!)!; return UserAvatar(account: account); }, ); diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index a1537ce..8990387 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -198,8 +198,10 @@ class _DocumentUploadPreparationPageState ), ), // Correspondent - if (LocalUserAccount - .current.paperlessUser.canViewCorrespondents) + if (context + .watch() + .paperlessUser + .canViewCorrespondents) LabelFormField( showAnyAssignedOption: false, showNotAssignedOption: false, @@ -220,11 +222,16 @@ class _DocumentUploadPreparationPageState options: state.correspondents, prefixIcon: const Icon(Icons.person_outline), allowSelectUnassigned: true, - canCreateNewLabel: LocalUserAccount - .current.paperlessUser.canCreateCorrespondents, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateCorrespondents, ), // Document type - if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes) + if (context + .watch() + .paperlessUser + .canViewDocumentTypes) LabelFormField( showAnyAssignedOption: false, showNotAssignedOption: false, @@ -245,10 +252,12 @@ class _DocumentUploadPreparationPageState options: state.documentTypes, prefixIcon: const Icon(Icons.description_outlined), allowSelectUnassigned: true, - canCreateNewLabel: LocalUserAccount - .current.paperlessUser.canCreateDocumentTypes, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateDocumentTypes, ), - if (LocalUserAccount.current.paperlessUser.canViewTags) + if (context.watch().paperlessUser.canViewTags) TagsFormField( name: DocumentModel.tagsKey, allowCreation: true, @@ -296,7 +305,7 @@ class _DocumentUploadPreparationPageState ), userId: Hive.box(HiveBoxes.globalSettings) .getValue()! - .currentLoggedInUser!, + .loggedInUserId!, title: title, documentType: docType, correspondent: correspondent, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 4cdbd32..54444f0 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -23,6 +23,7 @@ import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart'; import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class DocumentFilterIntent { final DocumentFilter? filter; @@ -55,7 +56,7 @@ class _DocumentsPageState extends State void initState() { super.initState(); final showSavedViews = - LocalUserAccount.current.paperlessUser.canViewSavedViews; + context.read().paperlessUser.canViewSavedViews; _tabController = TabController( length: showSavedViews ? 2 : 1, vsync: this, @@ -116,7 +117,7 @@ class _DocumentsPageState extends State return SafeArea( top: true, child: Scaffold( - drawer: const AppDrawer(), + drawer: AppDrawer(), floatingActionButton: BlocBuilder( builder: (context, state) { final appliedFiltersCount = state.filter.appliedFiltersCount; @@ -232,7 +233,9 @@ class _DocumentsPageState extends State controller: _tabController, tabs: [ Tab(text: S.of(context)!.documents), - if (LocalUserAccount.current.paperlessUser + if (context + .watch() + .paperlessUser .canViewSavedViews) Tab(text: S.of(context)!.views), ], @@ -276,8 +279,10 @@ class _DocumentsPageState extends State ); }, ), - if (LocalUserAccount - .current.paperlessUser.canViewSavedViews) + if (context + .watch() + .paperlessUser + .canViewSavedViews) Builder( builder: (context) { return _buildSavedViewsTab( @@ -378,7 +383,9 @@ class _DocumentsPageState extends State final allowToggleFilter = state.selection.isEmpty; return SliverAdaptiveDocumentsView( viewType: state.viewType, - onTap: _openDetails, + onTap: (document) { + DocumentDetailsRoute($extra: document).push(context); + }, onSelected: context.read().toggleDocumentSelection, hasInternetConnection: connectivityState.isConnected, @@ -488,13 +495,6 @@ class _DocumentsPageState extends State } } - void _openDetails(DocumentModel document) { - pushDocumentDetailsRoute( - context, - document: document, - ); - } - void _addTagToFilter(int tagId) { final cubit = context.read(); try { diff --git a/lib/features/documents/view/widgets/items/document_detailed_item.dart b/lib/features/documents/view/widgets/items/document_detailed_item.dart index 3f214ae..8fa60df 100644 --- a/lib/features/documents/view/widgets/items/document_detailed_item.dart +++ b/lib/features/documents/view/widgets/items/document_detailed_item.dart @@ -2,7 +2,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:hive_flutter/adapters.dart'; import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; @@ -32,6 +37,12 @@ class DocumentDetailedItem extends DocumentItem { @override Widget build(BuildContext context) { + final currentUserId = Hive.box(HiveBoxes.globalSettings) + .getValue()! + .loggedInUserId; + final paperlessUser = Hive.box(HiveBoxes.localUserAccount) + .get(currentUserId)! + .paperlessUser; final size = MediaQuery.of(context).size; final insets = MediaQuery.of(context).viewInsets; final padding = MediaQuery.of(context).viewPadding; @@ -104,48 +115,51 @@ class DocumentDetailedItem extends DocumentItem { maxLines: 2, overflow: TextOverflow.ellipsis, ).paddedLTRB(8, 0, 8, 4), - Row( - children: [ - const Icon( - Icons.person_outline, - size: 16, - ).paddedOnly(right: 4.0), - CorrespondentWidget( - onSelected: onCorrespondentSelected, - textStyle: Theme.of(context).textTheme.titleSmall?.apply( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - correspondent: context - .watch() - .state - .correspondents[document.correspondent], - ), - ], - ).paddedLTRB(8, 0, 8, 4), - Row( - children: [ - const Icon( - Icons.description_outlined, - size: 16, - ).paddedOnly(right: 4.0), - DocumentTypeWidget( - onSelected: onDocumentTypeSelected, - textStyle: Theme.of(context).textTheme.titleSmall?.apply( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - documentType: context - .watch() - .state - .documentTypes[document.documentType], - ), - ], - ).paddedLTRB(8, 0, 8, 4), - TagsWidget( - tags: document.tags - .map((e) => context.watch().state.tags[e]!) - .toList(), - onTagSelected: onTagSelected, - ).padded(), + if (paperlessUser.canViewCorrespondents) + Row( + children: [ + const Icon( + Icons.person_outline, + size: 16, + ).paddedOnly(right: 4.0), + CorrespondentWidget( + onSelected: onCorrespondentSelected, + textStyle: Theme.of(context).textTheme.titleSmall?.apply( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + correspondent: context + .watch() + .state + .correspondents[document.correspondent], + ), + ], + ).paddedLTRB(8, 0, 8, 4), + if (paperlessUser.canViewDocumentTypes) + Row( + children: [ + const Icon( + Icons.description_outlined, + size: 16, + ).paddedOnly(right: 4.0), + DocumentTypeWidget( + onSelected: onDocumentTypeSelected, + textStyle: Theme.of(context).textTheme.titleSmall?.apply( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + documentType: context + .watch() + .state + .documentTypes[document.documentType], + ), + ], + ).paddedLTRB(8, 0, 8, 4), + if (paperlessUser.canViewTags) + TagsWidget( + tags: document.tags + .map((e) => context.watch().state.tags[e]!) + .toList(), + onTagSelected: onTagSelected, + ).padded(), if (highlights != null) Html( data: '

${highlights!}

', diff --git a/lib/features/documents/view/widgets/search/document_filter_form.dart b/lib/features/documents/view/widgets/search/document_filter_form.dart index 41a9ca0..29cb7a0 100644 --- a/lib/features/documents/view/widgets/search/document_filter_form.dart +++ b/lib/features/documents/view/widgets/search/document_filter_form.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; @@ -160,8 +161,10 @@ class _DocumentFilterFormState extends State { initialValue: widget.initialFilter.documentType, prefixIcon: const Icon(Icons.description_outlined), allowSelectUnassigned: false, - canCreateNewLabel: - LocalUserAccount.current.paperlessUser.canCreateDocumentTypes, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateDocumentTypes, ); } @@ -173,8 +176,10 @@ class _DocumentFilterFormState extends State { initialValue: widget.initialFilter.correspondent, prefixIcon: const Icon(Icons.person_outline), allowSelectUnassigned: false, - canCreateNewLabel: - LocalUserAccount.current.paperlessUser.canCreateCorrespondents, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateCorrespondents, ); } @@ -187,7 +192,7 @@ class _DocumentFilterFormState extends State { prefixIcon: const Icon(Icons.folder_outlined), allowSelectUnassigned: false, canCreateNewLabel: - LocalUserAccount.current.paperlessUser.canCreateStoragePaths, + context.watch().paperlessUser.canCreateStoragePaths, ); } diff --git a/lib/features/edit_label/view/impl/add_storage_path_page.dart b/lib/features/edit_label/view/impl/add_storage_path_page.dart index 3e2d311..b033a72 100644 --- a/lib/features/edit_label/view/impl/add_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/add_storage_path_page.dart @@ -7,8 +7,8 @@ import 'package:paperless_mobile/features/labels/storage_path/view/widgets/stora import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddStoragePathPage extends StatelessWidget { - final String? initalName; - const AddStoragePathPage({Key? key, this.initalName}) : super(key: key); + final String? initialName; + const AddStoragePathPage({Key? key, this.initialName}) : super(key: key); @override Widget build(BuildContext context) { @@ -19,7 +19,7 @@ class AddStoragePathPage extends StatelessWidget { child: AddLabelPage( pageTitle: Text(S.of(context)!.addStoragePath), fromJsonT: StoragePath.fromJson, - initialName: initalName, + initialName: initialName, onSubmit: (context, label) => context.read().addStoragePath(label), additionalFields: const [ diff --git a/lib/features/edit_label/view/impl/add_tag_page.dart b/lib/features/edit_label/view/impl/add_tag_page.dart index 3a13501..70ae638 100644 --- a/lib/features/edit_label/view/impl/add_tag_page.dart +++ b/lib/features/edit_label/view/impl/add_tag_page.dart @@ -10,8 +10,8 @@ import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class AddTagPage extends StatelessWidget { - final String? initialValue; - const AddTagPage({Key? key, this.initialValue}) : super(key: key); + final String? initialName; + const AddTagPage({Key? key, this.initialName}) : super(key: key); @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class AddTagPage extends StatelessWidget { child: AddLabelPage( pageTitle: Text(S.of(context)!.addTag), fromJsonT: Tag.fromJson, - initialName: initialValue, + initialName: initialName, onSubmit: (context, label) => context.read().addTag(label), additionalFields: [ diff --git a/lib/features/edit_label/view/impl/edit_correspondent_page.dart b/lib/features/edit_label/view/impl/edit_correspondent_page.dart index 1dce099..c358cd8 100644 --- a/lib/features/edit_label/view/impl/edit_correspondent_page.dart +++ b/lib/features/edit_label/view/impl/edit_correspondent_page.dart @@ -24,8 +24,10 @@ class EditCorrespondentPage extends StatelessWidget { context.read().replaceCorrespondent(label), onDelete: (context, label) => context.read().removeCorrespondent(label), - canDelete: - LocalUserAccount.current.paperlessUser.canDeleteCorrespondents, + canDelete: context + .watch() + .paperlessUser + .canDeleteCorrespondents, ); }), ); diff --git a/lib/features/edit_label/view/impl/edit_document_type_page.dart b/lib/features/edit_label/view/impl/edit_document_type_page.dart index afd2f7f..824e0e7 100644 --- a/lib/features/edit_label/view/impl/edit_document_type_page.dart +++ b/lib/features/edit_label/view/impl/edit_document_type_page.dart @@ -22,8 +22,10 @@ class EditDocumentTypePage extends StatelessWidget { context.read().replaceDocumentType(label), onDelete: (context, label) => context.read().removeDocumentType(label), - canDelete: - LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes, + canDelete: context + .watch() + .paperlessUser + .canDeleteDocumentTypes, ), ); } diff --git a/lib/features/edit_label/view/impl/edit_storage_path_page.dart b/lib/features/edit_label/view/impl/edit_storage_path_page.dart index 3a56b55..91d512c 100644 --- a/lib/features/edit_label/view/impl/edit_storage_path_page.dart +++ b/lib/features/edit_label/view/impl/edit_storage_path_page.dart @@ -23,7 +23,10 @@ class EditStoragePathPage extends StatelessWidget { context.read().replaceStoragePath(label), onDelete: (context, label) => context.read().removeStoragePath(label), - canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths, + canDelete: context + .watch() + .paperlessUser + .canDeleteStoragePaths, additionalFields: [ StoragePathAutofillFormBuilderField( name: StoragePath.pathKey, diff --git a/lib/features/edit_label/view/impl/edit_tag_page.dart b/lib/features/edit_label/view/impl/edit_tag_page.dart index fbd62af..48fa3bd 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -26,7 +26,8 @@ class EditTagPage extends StatelessWidget { context.read().replaceTag(label), onDelete: (context, label) => context.read().removeTag(label), - canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags, + canDelete: + context.watch().paperlessUser.canDeleteTags, additionalFields: [ FormBuilderColorPickerField( initialValue: tag.color, diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 0a26600..d0ce3ce 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; @@ -68,7 +69,7 @@ class _LabelFormState extends State> { Widget build(BuildContext context) { List selectableMatchingAlgorithmValues = getSelectableMatchingAlgorithmValues( - context.watch().hasMultiUserSupport, + context.watch().hasMultiUserSupport, ); return Scaffold( resizeToAvoidBottomInset: false, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart deleted file mode 100644 index ae05192..0000000 --- a/lib/features/home/view/home_page.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hive/hive.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/config/hive/hive_config.dart'; -import 'package:paperless_mobile/core/database/tables/global_settings.dart'; -import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/core/global/constants.dart'; -import 'package:paperless_mobile/core/navigation/push_routes.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/service/file_description.dart'; -import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; -import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart'; -import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; -import 'package:paperless_mobile/features/home/view/route_description.dart'; -import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; -import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; -import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; -import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; -import 'package:paperless_mobile/features/sharing/share_intent_queue.dart'; -import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; -import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:responsive_builder/responsive_builder.dart'; - -/// Wrapper around all functionality for a logged in user. -/// Performs initialization logic. -class HomePage extends StatefulWidget { - final int paperlessApiVersion; - const HomePage({Key? key, required this.paperlessApiVersion}) - : super(key: key); - - @override - _HomePageState createState() => _HomePageState(); -} - -class _HomePageState extends State with WidgetsBindingObserver { - int _currentIndex = 0; - Timer? _inboxTimer; - late final StreamSubscription _shareMediaSubscription; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - - final currentUser = Hive.box(HiveBoxes.globalSettings) - .getValue()! - .currentLoggedInUser!; - // For sharing files coming from outside the app while the app is still opened - _shareMediaSubscription = ReceiveSharingIntent.getMediaStream().listen( - (files) => - ShareIntentQueue.instance.addAll(files, userId: currentUser)); - // For sharing files coming from outside the app while the app is closed - ReceiveSharingIntent.getInitialMedia().then((files) => - ShareIntentQueue.instance.addAll(files, userId: currentUser)); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _listenForReceivedFiles(); - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } - - void _listenToInboxChanges() { - if (LocalUserAccount.current.paperlessUser.canViewTags) { - _inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) { - if (!mounted) { - timer.cancel(); - } else { - context.read().refreshItemsInInboxCount(); - } - }); - } - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.resumed: - log('App is now in foreground'); - context.read().reload(); - log("Reloaded device connectivity state"); - if (!(_inboxTimer?.isActive ?? true)) { - _listenToInboxChanges(); - } - break; - case AppLifecycleState.inactive: - case AppLifecycleState.paused: - case AppLifecycleState.detached: - default: - log('App is now in background'); - _inboxTimer?.cancel(); - break; - } - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - _inboxTimer?.cancel(); - _shareMediaSubscription.cancel(); - super.dispose(); - } - - void _listenForReceivedFiles() async { - final currentUser = Hive.box(HiveBoxes.globalSettings) - .getValue()! - .currentLoggedInUser!; - if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) { - await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!); - } - ShareIntentQueue.instance.addListener(() async { - final queue = ShareIntentQueue.instance; - while (queue.userHasUnhandlesFiles(currentUser)) { - final file = queue.pop(currentUser)!; - await _handleReceivedFile(file); - } - }); - } - - bool _isFileTypeSupported(SharedMediaFile file) { - return supportedFileExtensions.contains( - file.path.split('.').last.toLowerCase(), - ); - } - - Future _handleReceivedFile(final SharedMediaFile file) async { - SharedMediaFile mediaFile; - if (Platform.isIOS) { - // Workaround for file not found on iOS: https://stackoverflow.com/a/72813212 - mediaFile = SharedMediaFile( - file.path.replaceAll('file://', ''), - file.thumbnail, - file.duration, - file.type, - ); - } else { - mediaFile = file; - } - debugPrint("Consuming media file: ${mediaFile.path}"); - if (!_isFileTypeSupported(mediaFile)) { - Fluttertoast.showToast( - msg: translateError(context, ErrorCode.unsupportedFileFormat), - ); - if (Platform.isAndroid) { - // As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines. - await SystemNavigator.pop(); - } - return; - } - - if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) { - Fluttertoast.showToast( - msg: "You do not have the permissions to upload documents.", - ); - return; - } - final fileDescription = FileDescription.fromPath(mediaFile.path); - if (await File(mediaFile.path).exists()) { - final bytes = await File(mediaFile.path).readAsBytes(); - final result = await pushDocumentUploadPreparationPage( - context, - bytes: bytes, - filename: fileDescription.filename, - title: fileDescription.filename, - fileExtension: fileDescription.extension, - ); - if (result?.success ?? false) { - await Fluttertoast.showToast( - msg: S.of(context)!.documentSuccessfullyUploadedProcessing, - ); - SystemNavigator.pop(); - } - } else { - Fluttertoast.showToast( - msg: S.of(context)!.couldNotAccessReceivedFile, - toastLength: Toast.LENGTH_LONG, - ); - } - } - - @override - Widget build(BuildContext context) { - final destinations = [ - RouteDescription( - icon: const Icon(Icons.description_outlined), - selectedIcon: Icon( - Icons.description, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context)!.documents, - ), - if (LocalUserAccount.current.paperlessUser.canCreateDocuments) - RouteDescription( - icon: const Icon(Icons.document_scanner_outlined), - selectedIcon: Icon( - Icons.document_scanner, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context)!.scanner, - ), - RouteDescription( - icon: const Icon(Icons.sell_outlined), - selectedIcon: Icon( - Icons.sell, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context)!.labels, - ), - if (LocalUserAccount.current.paperlessUser.canViewTags) - RouteDescription( - icon: const Icon(Icons.inbox_outlined), - selectedIcon: Icon( - Icons.inbox, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context)!.inbox, - badgeBuilder: (icon) => BlocBuilder( - builder: (context, state) { - return Badge.count( - isLabelVisible: state.itemsInInboxCount > 0, - count: state.itemsInInboxCount, - child: icon, - ); - }, - ), - ), - ]; - final routes = [ - const DocumentsPage(), - if (LocalUserAccount.current.paperlessUser.canCreateDocuments) - const ScannerPage(), - const LabelsPage(), - if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(), - ]; - return MultiBlocListener( - listeners: [ - BlocListener( - // If app was started offline, load data once it comes back online. - listenWhen: (previous, current) => - previous != ConnectivityState.connected && - current == ConnectivityState.connected, - listener: (context, state) async { - try { - debugPrint( - "[HomePage] BlocListener#listener: " - "Loading saved views and labels...", - ); - await Future.wait([ - context.read().initialize(), - context.read().initialize(), - ]); - debugPrint("[HomePage] BlocListener#listener: " - "Saved views and labels successfully loaded."); - } catch (error, stackTrace) { - debugPrint( - '[HomePage] BlocListener.listener: ' - 'An error occurred while loading saved views and labels.\n' - '${error.toString()}', - ); - debugPrintStack(stackTrace: stackTrace); - } - }, - ), - BlocListener( - listener: (context, state) { - if (state.task != null) { - // Handle local notifications on task change (only when app is running for now). - context - .read() - .notifyTaskChanged(state.task!); - } - }, - ), - ], - child: ResponsiveBuilder( - builder: (context, sizingInformation) { - if (!sizingInformation.isMobile) { - return Scaffold( - body: Row( - children: [ - NavigationRail( - labelType: NavigationRailLabelType.all, - destinations: destinations - .map((e) => e.toNavigationRailDestination()) - .toList(), - selectedIndex: _currentIndex, - onDestinationSelected: _onNavigationChanged, - ), - const VerticalDivider(thickness: 1, width: 1), - Expanded( - child: routes[_currentIndex], - ), - ], - ), - ); - } - return Scaffold( - bottomNavigationBar: NavigationBar( - labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, - elevation: 4.0, - selectedIndex: _currentIndex, - onDestinationSelected: _onNavigationChanged, - destinations: - destinations.map((e) => e.toNavigationDestination()).toList(), - ), - body: routes[_currentIndex], - ); - }, - ), - ); - } - - void _onNavigationChanged(index) { - if (_currentIndex != index) { - setState(() => _currentIndex = index); - } - } -} diff --git a/lib/features/home/view/home_route.dart b/lib/features/home/view/home_route.dart deleted file mode 100644 index 7b3b082..0000000 --- a/lib/features/home/view/home_route.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:hive_flutter/adapters.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/config/hive/hive_config.dart'; -import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; -import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; -import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/repository/user_repository.dart'; -import 'package:paperless_mobile/core/security/session_manager.dart'; -import 'package:paperless_mobile/core/service/dio_file_service.dart'; -import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; -import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; -import 'package:paperless_mobile/features/home/view/home_page.dart'; -import 'package:paperless_mobile/features/home/view/model/api_version.dart'; -import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; -import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; -import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; -import 'package:provider/provider.dart'; - -class HomeRoute extends StatelessWidget { - /// The id of the currently authenticated user (e.g. demo@paperless.example.com) - final String localUserId; - - /// The Paperless API version of the currently connected instance - final int paperlessApiVersion; - - // A factory providing the API implementations given an API version - final PaperlessApiFactory paperlessProviderFactory; - - const HomeRoute({ - super.key, - required this.paperlessApiVersion, - required this.paperlessProviderFactory, - required this.localUserId, - }); - - @override - Widget build(BuildContext context) { - return GlobalSettingsBuilder( - builder: (context, settings) { - final currentLocalUserId = settings.currentLoggedInUser; - if (currentLocalUserId == null) { - // This is the case when the current user logs out of the app. - return SizedBox.shrink(); - } - final currentUser = - Hive.box(HiveBoxes.localUserAccount) - .get(currentLocalUserId)!; - final apiVersion = ApiVersion(paperlessApiVersion); - return MultiProvider( - providers: [ - Provider.value(value: apiVersion), - Provider( - create: (context) => CacheManager( - Config( - // Isolated cache per user. - localUserId, - fileService: - DioFileService(context.read().client), - ), - ), - ), - ProxyProvider( - update: (context, value, previous) => - paperlessProviderFactory.createDocumentsApi( - value.client, - apiVersion: paperlessApiVersion, - ), - ), - ProxyProvider( - update: (context, value, previous) => - paperlessProviderFactory.createLabelsApi( - value.client, - apiVersion: paperlessApiVersion, - ), - ), - ProxyProvider( - update: (context, value, previous) => - paperlessProviderFactory.createSavedViewsApi( - value.client, - apiVersion: paperlessApiVersion, - ), - ), - ProxyProvider( - update: (context, value, previous) => - paperlessProviderFactory.createServerStatsApi( - value.client, - apiVersion: paperlessApiVersion, - ), - ), - ProxyProvider( - update: (context, value, previous) => - paperlessProviderFactory.createTasksApi( - value.client, - apiVersion: paperlessApiVersion, - ), - ), - if (apiVersion.hasMultiUserSupport) - ProxyProvider( - update: (context, value, previous) => PaperlessUserApiV3Impl( - value.client, - ), - ), - ], - builder: (context, child) { - return MultiProvider( - providers: [ - ProxyProvider( - update: (context, value, previous) { - final repo = LabelRepository(value); - if (currentUser.paperlessUser.canViewCorrespondents) { - repo.findAllCorrespondents(); - } - if (currentUser.paperlessUser.canViewDocumentTypes) { - repo.findAllDocumentTypes(); - } - if (currentUser.paperlessUser.canViewTags) { - repo.findAllTags(); - } - if (currentUser.paperlessUser.canViewStoragePaths) { - repo.findAllStoragePaths(); - } - return repo; - }, - ), - ProxyProvider( - update: (context, value, previous) { - final repo = SavedViewRepository(value); - if (currentUser.paperlessUser.canViewSavedViews) { - repo.initialize(); - } - return repo; - }, - ), - ], - builder: (context, child) { - return MultiProvider( - providers: [ - ProxyProvider3< - PaperlessDocumentsApi, - DocumentChangedNotifier, - LabelRepository, - DocumentsCubit>( - update: - (context, docApi, notifier, labelRepo, previous) => - DocumentsCubit( - docApi, - notifier, - labelRepo, - Hive.box(HiveBoxes.localUserAppState) - .get(currentLocalUserId)!, - )..initialize(), - ), - Provider( - create: (context) => - DocumentScannerCubit(context.read())), - ProxyProvider4< - PaperlessDocumentsApi, - PaperlessServerStatsApi, - LabelRepository, - DocumentChangedNotifier, - InboxCubit>( - update: (context, docApi, statsApi, labelRepo, notifier, - previous) => - InboxCubit( - docApi, - statsApi, - labelRepo, - notifier, - )..initialize(), - ), - ProxyProvider( - update: (context, savedViewRepo, previous) => - SavedViewCubit(savedViewRepo), - ), - ProxyProvider( - update: (context, value, previous) => LabelCubit(value), - ), - ProxyProvider( - update: (context, value, previous) => - TaskStatusCubit(value), - ), - if (paperlessApiVersion >= 3) - ProxyProvider( - update: (context, value, previous) => - UserRepository(value)..initialize(), - ), - ], - child: HomePage(paperlessApiVersion: paperlessApiVersion), - ); - }, - ); - }, - ); - }, - ); - } -} diff --git a/lib/features/home/view/home_shell_widget.dart b/lib/features/home/view/home_shell_widget.dart new file mode 100644 index 0000000..69c8a95 --- /dev/null +++ b/lib/features/home/view/home_shell_widget.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'; +import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; +import 'package:paperless_mobile/core/repository/user_repository.dart'; +import 'package:paperless_mobile/core/security/session_manager.dart'; +import 'package:paperless_mobile/core/service/dio_file_service.dart'; +import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart'; +import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; +import 'package:paperless_mobile/features/home/view/model/api_version.dart'; +import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; +import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; +import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; +import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; +import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart'; +import 'package:provider/provider.dart'; + +class HomeShellWidget extends StatelessWidget { + /// The id of the currently authenticated user (e.g. demo@paperless.example.com) + final String localUserId; + + /// The Paperless API version of the currently connected instance + final int paperlessApiVersion; + + // A factory providing the API implementations given an API version + final PaperlessApiFactory paperlessProviderFactory; + + final Widget child; + + const HomeShellWidget({ + super.key, + required this.paperlessApiVersion, + required this.paperlessProviderFactory, + required this.localUserId, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return GlobalSettingsBuilder( + builder: (context, settings) { + final currentUserId = settings.loggedInUserId; + if (currentUserId == null) { + // This is the case when the current user logs out of the app. + return SizedBox.shrink(); + } + final apiVersion = ApiVersion(paperlessApiVersion); + return ValueListenableBuilder( + valueListenable: + Hive.box(HiveBoxes.localUserAccount) + .listenable(keys: [currentUserId]), + builder: (context, box, _) { + final currentLocalUser = box.get(currentUserId)!; + return MultiProvider( + providers: [ + Provider.value(value: currentLocalUser), + Provider.value(value: apiVersion), + Provider( + create: (context) => CacheManager( + Config( + // Isolated cache per user. + localUserId, + fileService: + DioFileService(context.read().client), + ), + ), + ), + Provider( + create: (context) => + paperlessProviderFactory.createDocumentsApi( + context.read().client, + apiVersion: paperlessApiVersion, + ), + ), + Provider( + create: (context) => paperlessProviderFactory.createLabelsApi( + context.read().client, + apiVersion: paperlessApiVersion, + ), + ), + Provider( + create: (context) => + paperlessProviderFactory.createSavedViewsApi( + context.read().client, + apiVersion: paperlessApiVersion, + ), + ), + Provider( + create: (context) => + paperlessProviderFactory.createServerStatsApi( + context.read().client, + apiVersion: paperlessApiVersion, + ), + ), + Provider( + create: (context) => paperlessProviderFactory.createTasksApi( + context.read().client, + apiVersion: paperlessApiVersion, + ), + ), + if (currentLocalUser.hasMultiUserSupport) + Provider( + create: (context) => PaperlessUserApiV3Impl( + context.read().client, + ), + ), + ], + builder: (context, _) { + return MultiProvider( + providers: [ + Provider( + create: (context) { + final repo = LabelRepository(context.read()); + if (currentLocalUser + .paperlessUser.canViewCorrespondents) { + repo.findAllCorrespondents(); + } + if (currentLocalUser + .paperlessUser.canViewDocumentTypes) { + repo.findAllDocumentTypes(); + } + if (currentLocalUser.paperlessUser.canViewTags) { + repo.findAllTags(); + } + if (currentLocalUser + .paperlessUser.canViewStoragePaths) { + repo.findAllStoragePaths(); + } + return repo; + }, + ), + Provider( + create: (context) { + final repo = SavedViewRepository(context.read()); + if (currentLocalUser.paperlessUser.canViewSavedViews) { + repo.initialize(); + } + return repo; + }, + ), + ], + builder: (context, _) { + return MultiProvider( + providers: [ + Provider( + create: (context) => DocumentsCubit( + context.read(), + context.read(), + context.read(), + Hive.box( + HiveBoxes.localUserAppState) + .get(currentUserId)!, + )..initialize(), + ), + Provider( + create: (context) => + DocumentScannerCubit(context.read()), + ), + if (currentLocalUser.paperlessUser.canViewDocuments && + currentLocalUser.paperlessUser.canViewTags) + Provider( + create: (context) => InboxCubit( + context.read(), + context.read(), + context.read(), + context.read(), + ).initialize(), + ), + Provider( + create: (context) => SavedViewCubit( + context.read(), + ), + ), + Provider( + create: (context) => LabelCubit( + context.read(), + ), + ), + Provider( + create: (context) => TaskStatusCubit( + context.read(), + ), + ), + if (currentLocalUser.hasMultiUserSupport) + Provider( + create: (context) => UserRepository( + context.read(), + )..initialize(), + ), + ], + child: child, + ); + }, + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/home/view/model/api_version.dart b/lib/features/home/view/model/api_version.dart index a7cabd1..1a70fcd 100644 --- a/lib/features/home/view/model/api_version.dart +++ b/lib/features/home/view/model/api_version.dart @@ -1,7 +1,7 @@ class ApiVersion { final int version; - ApiVersion(this.version); + const ApiVersion(this.version); - bool get hasMultiUserSupport => version >= 3; + } diff --git a/lib/features/home/view/scaffold_with_navigation_bar.dart b/lib/features/home/view/scaffold_with_navigation_bar.dart new file mode 100644 index 0000000..518b8d3 --- /dev/null +++ b/lib/features/home/view/scaffold_with_navigation_bar.dart @@ -0,0 +1,168 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; + +const _landingPage = 0; +const _documentsIndex = 1; +const _scannerIndex = 2; +const _labelsIndex = 3; +const _inboxIndex = 4; + +class ScaffoldWithNavigationBar extends StatefulWidget { + final UserModel authenticatedUser; + final StatefulNavigationShell navigationShell; + const ScaffoldWithNavigationBar({ + super.key, + required this.authenticatedUser, + required this.navigationShell, + }); + + @override + State createState() => + ScaffoldWithNavigationBarState(); +} + +class ScaffoldWithNavigationBarState extends State { + @override + Widget build(BuildContext context) { + final disabledColor = Theme.of(context).disabledColor; + final primaryColor = Theme.of(context).colorScheme.primary; + return Scaffold( + drawer: const AppDrawer(), + bottomNavigationBar: NavigationBar( + selectedIndex: widget.navigationShell.currentIndex, + onDestinationSelected: (index) { + switch (index) { + case _landingPage: + widget.navigationShell.goBranch(index); + break; + case _documentsIndex: + if (widget.authenticatedUser.canViewDocuments) { + widget.navigationShell.goBranch(index); + } else { + showSnackBar( + context, "You do not have permission to access this page."); + } + break; + case _scannerIndex: + if (widget.authenticatedUser.canCreateDocuments) { + widget.navigationShell.goBranch(index); + } else { + showSnackBar( + context, "You do not have permission to access this page."); + } + break; + case _labelsIndex: + if (widget.authenticatedUser.canViewAnyLabel) { + widget.navigationShell.goBranch(index); + } else { + showSnackBar( + context, "You do not have permission to access this page."); + } + break; + case _inboxIndex: + if (widget.authenticatedUser.canViewDocuments && + widget.authenticatedUser.canViewTags) { + widget.navigationShell.goBranch(index); + } else { + showSnackBar( + context, "You do not have permission to access this page."); + } + break; + default: + break; + } + }, + destinations: [ + NavigationDestination( + icon: Icon(Icons.home_outlined), + selectedIcon: Icon( + Icons.home, + color: primaryColor, + ), + label: "Home", //TODO: INTL + ), + NavigationDestination( + icon: Icon( + Icons.description_outlined, + color: !widget.authenticatedUser.canViewDocuments + ? disabledColor + : null, + ), + selectedIcon: Icon( + Icons.description, + color: primaryColor, + ), + label: S.of(context)!.documents, + ), + NavigationDestination( + icon: Icon( + Icons.document_scanner_outlined, + color: !widget.authenticatedUser.canCreateDocuments + ? disabledColor + : null, + ), + selectedIcon: Icon( + Icons.document_scanner, + color: primaryColor, + ), + label: S.of(context)!.scanner, + ), + NavigationDestination( + icon: Icon( + Icons.sell_outlined, + color: !widget.authenticatedUser.canViewAnyLabel + ? disabledColor + : null, + ), + selectedIcon: Icon( + Icons.sell, + color: primaryColor, + ), + label: S.of(context)!.labels, + ), + NavigationDestination( + icon: Builder(builder: (context) { + if (!(widget.authenticatedUser.canViewDocuments && + widget.authenticatedUser.canViewTags)) { + return Icon( + Icons.close, + color: disabledColor, + ); + } + return BlocBuilder( + builder: (context, state) { + return Badge.count( + isLabelVisible: state.itemsInInboxCount > 0, + count: state.itemsInInboxCount, + child: const Icon(Icons.inbox_outlined), + ); + }, + ); + }), + selectedIcon: BlocBuilder( + builder: (context, state) { + return Badge.count( + isLabelVisible: state.itemsInInboxCount > 0, + count: state.itemsInInboxCount, + child: Icon( + Icons.inbox, + color: primaryColor, + ), + ); + }, + ), + label: S.of(context)!.inbox, + ), + ], + ), + body: widget.navigationShell, + ); + } +} diff --git a/lib/features/inbox/cubit/inbox_cubit.dart b/lib/features/inbox/cubit/inbox_cubit.dart index 3356fd5..ffee09c 100644 --- a/lib/features/inbox/cubit/inbox_cubit.dart +++ b/lib/features/inbox/cubit/inbox_cubit.dart @@ -61,6 +61,7 @@ class InboxCubit extends HydratedCubit Future initialize() async { await refreshItemsInInboxCount(false); await loadInbox(); + super.initialize(); } Future refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 32b7934..5ee407d 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -39,7 +39,7 @@ class _InboxPageState extends State @override Widget build(BuildContext context) { final canEditDocument = - LocalUserAccount.current.paperlessUser.canEditDocuments; + context.watch().paperlessUser.canEditDocuments; return Scaffold( drawer: const AppDrawer(), floatingActionButton: BlocBuilder( diff --git a/lib/features/inbox/view/widgets/inbox_item.dart b/lib/features/inbox/view/widgets/inbox_item.dart index c0da605..656088f 100644 --- a/lib/features/inbox/view/widgets/inbox_item.dart +++ b/lib/features/inbox/view/widgets/inbox_item.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; @@ -15,6 +16,7 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class InboxItemPlaceholder extends StatelessWidget { const InboxItemPlaceholder({super.key}); @@ -150,11 +152,10 @@ class _InboxItemState extends State { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - pushDocumentDetailsRoute( - context, - document: widget.document, + DocumentDetailsRoute( + $extra: widget.document, isLabelClickable: false, - ); + ).push(context); }, child: SizedBox( height: 200, @@ -238,8 +239,9 @@ class _InboxItemState extends State { } Widget _buildActions(BuildContext context) { - final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments; - final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments; + final currentUser = context.watch().paperlessUser; + final canEdit = currentUser.canEditDocuments; + final canDelete = currentUser.canDeleteDocuments; final chipShape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(32), ); diff --git a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart index 1a2b51e..b583256 100644 --- a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart +++ b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart @@ -191,7 +191,7 @@ class _FullscreenTagsFormState extends State { final createdTag = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => AddTagPage( - initialValue: _textEditingController.text, + initialName: _textEditingController.text, ), ), ); diff --git a/lib/features/labels/tags/view/widgets/tags_form_field.dart b/lib/features/labels/tags/view/widgets/tags_form_field.dart index 1339f70..7ec3096 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -1,6 +1,7 @@ import 'package:animations/animations.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; @@ -73,7 +74,7 @@ class TagsFormField extends StatelessWidget { initialValue: field.value, allowOnlySelection: allowOnlySelection, allowCreation: allowCreation && - LocalUserAccount.current.paperlessUser.canCreateTags, + context.watch().paperlessUser.canCreateTags, allowExclude: allowExclude, ), onClosed: (data) { diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 71bd9a4..5aa210b 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -7,23 +7,13 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; -import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; -import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; +import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; class LabelsPage extends StatefulWidget { const LabelsPage({Key? key}) : super(key: key); @@ -52,7 +42,7 @@ class _LabelsPageState extends State @override void initState() { super.initState(); - final user = LocalUserAccount.current.paperlessUser; + final user = context.read().paperlessUser; _tabController = TabController( length: _calculateTabCount(user), vsync: this) ..addListener(() => setState(() => _currentIndex = _tabController.index)); @@ -67,7 +57,7 @@ class _LabelsPageState extends State final currentUserId = Hive.box(HiveBoxes.globalSettings) .getValue()! - .currentLoggedInUser; + .loggedInUserId; final user = box.get(currentUserId)!.paperlessUser; return BlocBuilder( @@ -77,10 +67,14 @@ class _LabelsPageState extends State drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( onPressed: [ - if (user.canViewCorrespondents) _openAddCorrespondentPage, - if (user.canViewDocumentTypes) _openAddDocumentTypePage, - if (user.canViewTags) _openAddTagPage, - if (user.canViewStoragePaths) _openAddStoragePathPage, + if (user.canViewCorrespondents) + () => CreateLabelRoute().push(context), + if (user.canViewDocumentTypes) + () => CreateLabelRoute().push(context), + if (user.canViewTags) + () => CreateLabelRoute().push(context), + if (user.canViewStoragePaths) + () => CreateLabelRoute().push(context), ][_currentIndex], child: const Icon(Icons.add), ), @@ -213,144 +207,13 @@ class _LabelsPageState extends State controller: _tabController, children: [ if (user.canViewCorrespondents) - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector( - handle: tabBarHandle), - LabelTabView( - labels: state.correspondents, - filterBuilder: (label) => - DocumentFilter( - correspondent: - IdQueryParameter.fromId( - label.id!), - ), - canEdit: user.canEditCorrespondents, - canAddNew: - user.canCreateCorrespondents, - onEdit: _openEditCorrespondentPage, - emptyStateActionButtonLabel: S - .of(context)! - .addNewCorrespondent, - emptyStateDescription: S - .of(context)! - .noCorrespondentsSetUp, - onAddNew: _openAddCorrespondentPage, - ), - ], - ); - }, - ), + _buildCorrespondentsView(state, user), if (user.canViewDocumentTypes) - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector( - handle: tabBarHandle), - LabelTabView( - labels: state.documentTypes, - filterBuilder: (label) => - DocumentFilter( - documentType: - IdQueryParameter.fromId( - label.id!), - ), - canEdit: user.canEditDocumentTypes, - canAddNew: - user.canCreateDocumentTypes, - onEdit: _openEditDocumentTypePage, - emptyStateActionButtonLabel: S - .of(context)! - .addNewDocumentType, - emptyStateDescription: S - .of(context)! - .noDocumentTypesSetUp, - onAddNew: _openAddDocumentTypePage, - ), - ], - ); - }, - ), + _buildDocumentTypesView(state, user), if (user.canViewTags) - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector( - handle: tabBarHandle), - LabelTabView( - labels: state.tags, - filterBuilder: (label) => - DocumentFilter( - tags: TagsQuery.ids( - include: [label.id!]), - ), - canEdit: user.canEditTags, - canAddNew: user.canCreateTags, - onEdit: _openEditTagPage, - leadingBuilder: (t) => CircleAvatar( - backgroundColor: t.color, - child: t.isInboxTag - ? Icon( - Icons.inbox, - color: t.textColor, - ) - : null, - ), - emptyStateActionButtonLabel: - S.of(context)!.addNewTag, - emptyStateDescription: - S.of(context)!.noTagsSetUp, - onAddNew: _openAddTagPage, - ), - ], - ); - }, - ), + _buildTagsView(state, user), if (user.canViewStoragePaths) - Builder( - builder: (context) { - return CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: searchBarHandle), - SliverOverlapInjector( - handle: tabBarHandle), - LabelTabView( - labels: state.storagePaths, - onEdit: _openEditStoragePathPage, - filterBuilder: (label) => - DocumentFilter( - storagePath: - IdQueryParameter.fromId( - label.id!), - ), - canEdit: user.canEditStoragePaths, - canAddNew: - user.canCreateStoragePaths, - contentBuilder: (path) => - Text(path.path), - emptyStateActionButtonLabel: S - .of(context)! - .addNewStoragePath, - emptyStateDescription: S - .of(context)! - .noStoragePathsSetUp, - onAddNew: _openAddStoragePathPage, - ), - ], - ); - }, - ), + _buildStoragePathView(state, user), ], ), ), @@ -365,73 +228,121 @@ class _LabelsPageState extends State }); } - void _openEditCorrespondentPage(Correspondent correspondent) { - Navigator.push( - context, - _buildLabelPageRoute(EditCorrespondentPage(correspondent: correspondent)), + Widget _buildCorrespondentsView(LabelState state, UserModel user) { + return Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + labels: state.correspondents, + filterBuilder: (label) => DocumentFilter( + correspondent: IdQueryParameter.fromId(label.id!), + ), + canEdit: user.canEditCorrespondents, + canAddNew: user.canCreateCorrespondents, + onEdit: (correspondent) { + EditLabelRoute(correspondent).push(context); + }, + emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent, + emptyStateDescription: S.of(context)!.noCorrespondentsSetUp, + onAddNew: () => CreateLabelRoute().push(context), + ), + ], + ); + }, ); } - void _openEditDocumentTypePage(DocumentType docType) { - Navigator.push( - context, - _buildLabelPageRoute(EditDocumentTypePage(documentType: docType)), + Widget _buildDocumentTypesView(LabelState state, UserModel user) { + return Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + labels: state.documentTypes, + filterBuilder: (label) => DocumentFilter( + documentType: IdQueryParameter.fromId(label.id!), + ), + canEdit: user.canEditDocumentTypes, + canAddNew: user.canCreateDocumentTypes, + onEdit: (label) { + EditLabelRoute(label).push(context); + }, + emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType, + emptyStateDescription: S.of(context)!.noDocumentTypesSetUp, + onAddNew: () => CreateLabelRoute().push(context), + ), + ], + ); + }, ); } - void _openEditTagPage(Tag tag) { - Navigator.push( - context, - _buildLabelPageRoute(EditTagPage(tag: tag)), + Widget _buildTagsView(LabelState state, UserModel user) { + return Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + labels: state.tags, + filterBuilder: (label) => DocumentFilter( + tags: TagsQuery.ids(include: [label.id!]), + ), + canEdit: user.canEditTags, + canAddNew: user.canCreateTags, + onEdit: (label) { + EditLabelRoute(label).push(context); + }, + leadingBuilder: (t) => CircleAvatar( + backgroundColor: t.color, + child: t.isInboxTag + ? Icon( + Icons.inbox, + color: t.textColor, + ) + : null, + ), + emptyStateActionButtonLabel: S.of(context)!.addNewTag, + emptyStateDescription: S.of(context)!.noTagsSetUp, + onAddNew: () => CreateLabelRoute().push(context), + ), + ], + ); + }, ); } - void _openEditStoragePathPage(StoragePath path) { - Navigator.push( - context, - _buildLabelPageRoute(EditStoragePathPage( - storagePath: path, - )), - ); - } - - void _openAddCorrespondentPage() { - Navigator.push( - context, - _buildLabelPageRoute(const AddCorrespondentPage()), - ); - } - - void _openAddDocumentTypePage() { - Navigator.push( - context, - _buildLabelPageRoute(const AddDocumentTypePage()), - ); - } - - void _openAddTagPage() { - Navigator.push( - context, - _buildLabelPageRoute(const AddTagPage()), - ); - } - - void _openAddStoragePathPage() { - Navigator.push( - context, - _buildLabelPageRoute(const AddStoragePathPage()), - ); - } - - MaterialPageRoute _buildLabelPageRoute(Widget page) { - return MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()) - ], - child: page, - ), + Widget _buildStoragePathView(LabelState state, UserModel user) { + return Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector(handle: searchBarHandle), + SliverOverlapInjector(handle: tabBarHandle), + LabelTabView( + labels: state.storagePaths, + onEdit: (label) { + EditLabelRoute(label).push(context); + }, + filterBuilder: (label) => DocumentFilter( + storagePath: IdQueryParameter.fromId(label.id!), + ), + canEdit: user.canEditStoragePaths, + canAddNew: user.canCreateStoragePaths, + contentBuilder: (path) => Text(path.path), + emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath, + emptyStateDescription: S.of(context)!.noStoragePathsSetUp, + onAddNew: () => CreateLabelRoute().push(context), + ), + ], + ); + }, ); } } diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index de59604..ed10064 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; @@ -36,7 +37,7 @@ class LabelItem extends StatelessWidget { Widget _buildReferencedDocumentsWidget(BuildContext context) { final canOpen = (label.documentCount ?? 0) > 0 && - LocalUserAccount.current.paperlessUser.canViewDocuments; + context.watch().paperlessUser.canViewDocuments; return TextButton.icon( label: const Icon(Icons.link), icon: Text(formatMaxCount(label.documentCount)), diff --git a/lib/features/landing/view/landing_page.dart b/lib/features/landing/view/landing_page.dart new file mode 100644 index 0000000..18a2a9d --- /dev/null +++ b/lib/features/landing/view/landing_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; +import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; +import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +class LandingPage extends StatefulWidget { + const LandingPage({super.key}); + + @override + State createState() => _LandingPageState(); +} + +class _LandingPageState extends State { + final _searchBarHandle = SliverOverlapAbsorberHandle(); + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: _searchBarHandle, + sliver: SliverSearchBar( + floating: true, + titleText: S.of(context)!.documents, + ), + ), + ], + body: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverToBoxAdapter( + child: Text( + "Welcome!", + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/linked_documents/view/linked_documents_page.dart b/lib/features/linked_documents/view/linked_documents_page.dart index 889da31..37ab278 100644 --- a/lib/features/linked_documents/view/linked_documents_page.dart +++ b/lib/features/linked_documents/view/linked_documents_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; @@ -7,6 +8,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/view_ import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class LinkedDocumentsPage extends StatefulWidget { const LinkedDocumentsPage({super.key}); @@ -51,11 +53,10 @@ class _LinkedDocumentsPageState extends State isLoading: state.isLoading, hasLoaded: state.hasLoaded, onTap: (document) { - pushDocumentDetailsRoute( - context, - document: document, + DocumentDetailsRoute( + $extra: document, isLabelClickable: false, - ); + ).push(context); }, ), ], diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index fc11adf..76b5c24 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -55,12 +55,11 @@ class AuthenticationCubit extends Cubit { // Mark logged in user as currently active user. final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; - globalSettings.currentLoggedInUser = localUserId; + globalSettings.loggedInUserId = localUserId; await globalSettings.save(); emit( AuthenticationState.authenticated( - apiVersion: apiVersion, localUserId: localUserId, ), ); @@ -75,7 +74,8 @@ class AuthenticationCubit extends Cubit { emit(const AuthenticationState.switchingAccounts()); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; - if (globalSettings.currentLoggedInUser == localUserId) { + if (globalSettings.loggedInUserId == localUserId) { + emit(AuthenticationState.authenticated(localUserId: localUserId)); return; } final userAccountBox = @@ -112,7 +112,7 @@ class AuthenticationCubit extends Cubit { baseUrl: account.serverUrl, ); - globalSettings.currentLoggedInUser = localUserId; + globalSettings.loggedInUserId = localUserId; await globalSettings.save(); final apiVersion = await _getApiVersion(_sessionManager.client); @@ -126,7 +126,6 @@ class AuthenticationCubit extends Cubit { emit(AuthenticationState.authenticated( localUserId: localUserId, - apiVersion: apiVersion, )); }); } @@ -175,13 +174,14 @@ class AuthenticationCubit extends Cubit { ); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; - final localUserId = globalSettings.currentLoggedInUser; + final localUserId = globalSettings.loggedInUserId; if (localUserId == null) { _debugPrintMessage( "restoreSessionState", "There is nothing to restore.", ); // If there is nothing to restore, we can quit here. + emit(const AuthenticationState.unauthenticated()); return; } final localUserAccountBox = @@ -223,7 +223,7 @@ class AuthenticationCubit extends Cubit { final authentication = await withEncryptedBox( HiveBoxes.localUserCredentials, (box) { - return box.get(globalSettings.currentLoggedInUser!); + return box.get(globalSettings.loggedInUserId!); }); if (authentication == null) { @@ -261,7 +261,6 @@ class AuthenticationCubit extends Cubit { ); emit( AuthenticationState.authenticated( - apiVersion: apiVersion, localUserId: localUserId, ), ); @@ -279,7 +278,7 @@ class AuthenticationCubit extends Cubit { await _resetExternalState(); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; - globalSettings.currentLoggedInUser = null; + globalSettings.loggedInUserId = null; await globalSettings.save(); emit(const AuthenticationState.unauthenticated()); @@ -389,6 +388,7 @@ class AuthenticationCubit extends Cubit { settings: LocalUserSettings(), serverUrl: serverUrl, paperlessUser: serverUser, + apiVersion: apiVersion, ), ); _debugPrintMessage( diff --git a/lib/features/login/cubit/authentication_state.dart b/lib/features/login/cubit/authentication_state.dart index fbe0790..12530aa 100644 --- a/lib/features/login/cubit/authentication_state.dart +++ b/lib/features/login/cubit/authentication_state.dart @@ -2,12 +2,18 @@ part of 'authentication_cubit.dart'; @freezed class AuthenticationState with _$AuthenticationState { + const AuthenticationState._(); + const factory AuthenticationState.unauthenticated() = _Unauthenticated; const factory AuthenticationState.requriresLocalAuthentication() = _RequiresLocalAuthentication; const factory AuthenticationState.authenticated({ required String localUserId, - required int apiVersion, }) = _Authenticated; const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts; + + bool get isAuthenticated => maybeWhen( + authenticated: (_) => true, + orElse: () => false, + ); } diff --git a/lib/features/login/view/add_account_page.dart b/lib/features/login/view/add_account_page.dart new file mode 100644 index 0000000..72ed029 --- /dev/null +++ b/lib/features/login/view/add_account_page.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/exception/server_message_exception.dart'; +import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate.dart'; +import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; +import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; +import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; +import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; + +import 'widgets/login_pages/server_login_page.dart'; +import 'widgets/never_scrollable_scroll_behavior.dart'; + +class AddAccountPage extends StatefulWidget { + final FutureOr Function( + BuildContext context, + String username, + String password, + String serverUrl, + ClientCertificate? clientCertificate, + ) onSubmit; + + final String submitText; + final String titleString; + + final bool showLocalAccounts; + + const AddAccountPage({ + Key? key, + required this.onSubmit, + required this.submitText, + required this.titleString, + this.showLocalAccounts = false, + }) : super(key: key); + + @override + State createState() => _AddAccountPageState(); +} + +class _AddAccountPageState extends State { + final _formKey = GlobalKey(); + + final PageController _pageController = PageController(); + + @override + Widget build(BuildContext context) { + final localAccounts = + Hive.box(HiveBoxes.localUserAccount); + return Scaffold( + resizeToAvoidBottomInset: false, + body: FormBuilder( + key: _formKey, + child: PageView( + controller: _pageController, + scrollBehavior: NeverScrollableScrollBehavior(), + children: [ + if (widget.showLocalAccounts && localAccounts.isNotEmpty) + Scaffold( + appBar: AppBar( + title: Text(S.of(context)!.logInToExistingAccount), + ), + bottomNavigationBar: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + child: Text(S.of(context)!.goToLogin), + onPressed: () { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + ], + ), + ), + body: ListView.builder( + itemBuilder: (context, index) { + final account = localAccounts.values.elementAt(index); + return Card( + child: UserAccountListTile( + account: account, + onTap: () { + context + .read() + .switchAccount(account.id); + }, + ), + ); + }, + itemCount: localAccounts.length, + ), + ), + ServerConnectionPage( + titleText: widget.titleString, + formBuilderKey: _formKey, + onContinue: () { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + ServerLoginPage( + formBuilderKey: _formKey, + submitText: widget.submitText, + onSubmit: _login, + ), + ], + ), + ), + ); + } + + Future _login() async { + FocusScope.of(context).unfocus(); + if (_formKey.currentState?.saveAndValidate() ?? false) { + final form = _formKey.currentState!.value; + ClientCertificate? clientCert; + final clientCertFormModel = + form[ClientCertificateFormField.fkClientCertificate] + as ClientCertificateFormModel?; + if (clientCertFormModel != null) { + clientCert = ClientCertificate( + bytes: clientCertFormModel.bytes, + passphrase: clientCertFormModel.passphrase, + ); + } + final credentials = + form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; + try { + await widget.onSubmit( + context, + credentials.username!, + credentials.password!, + form[ServerAddressFormField.fkServerAddress], + clientCert, + ); + } on PaperlessApiException catch (error) { + showErrorMessage(context, error); + } on ServerMessageException catch (error) { + showLocalizedError(context, error.message); + } catch (error) { + showGenericError(context, error); + } + } + } +} diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 578585c..afae0fc 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -1,161 +1,78 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:go_router/go_router.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; -import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; -import 'package:paperless_mobile/core/exception/server_message_exception.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; -import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart'; -import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; -import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; +import 'package:paperless_mobile/features/login/view/add_account_page.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; -import 'widgets/login_pages/server_login_page.dart'; -import 'widgets/never_scrollable_scroll_behavior.dart'; +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); -class LoginPage extends StatefulWidget { - final FutureOr Function( + @override + Widget build(BuildContext context) { + return AddAccountPage( + titleString: S.of(context)!.connectToPaperless, + submitText: S.of(context)!.signIn, + onSubmit: _onLogin, + showLocalAccounts: true, + ); + } + + void _onLogin( BuildContext context, String username, String password, String serverUrl, ClientCertificate? clientCertificate, - ) onSubmit; - - final String submitText; - final String titleString; - - final bool showLocalAccounts; - - const LoginPage({ - Key? key, - required this.onSubmit, - required this.submitText, - required this.titleString, - this.showLocalAccounts = false, - }) : super(key: key); - - @override - State createState() => _LoginPageState(); -} - -class _LoginPageState extends State { - final _formKey = GlobalKey(); - - final PageController _pageController = PageController(); - - @override - Widget build(BuildContext context) { - final localAccounts = - Hive.box(HiveBoxes.localUserAccount); - return Scaffold( - resizeToAvoidBottomInset: false, - body: FormBuilder( - key: _formKey, - child: PageView( - controller: _pageController, - scrollBehavior: NeverScrollableScrollBehavior(), - children: [ - if (widget.showLocalAccounts && localAccounts.isNotEmpty) - Scaffold( - appBar: AppBar( - title: Text(S.of(context)!.logInToExistingAccount), - ), - bottomNavigationBar: BottomAppBar( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilledButton( - child: Text(S.of(context)!.goToLogin), - onPressed: () { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, - ), - ], - ), - ), - body: ListView.builder( - itemBuilder: (context, index) { - final account = localAccounts.values.elementAt(index); - return Card( - child: UserAccountListTile( - account: account, - onTap: () { - context - .read() - .switchAccount(account.id); - }, - ), - ); - }, - itemCount: localAccounts.length, - ), - ), - ServerConnectionPage( - titleText: widget.titleString, - formBuilderKey: _formKey, - onContinue: () { - _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - }, + ) async { + try { + await context.read().login( + credentials: LoginFormCredentials( + username: username, + password: password, ), - ServerLoginPage( - formBuilderKey: _formKey, - submitText: widget.submitText, - onSubmit: _login, - ), - ], - ), - ), - ); - } - - Future _login() async { - FocusScope.of(context).unfocus(); - if (_formKey.currentState?.saveAndValidate() ?? false) { - final form = _formKey.currentState!.value; - ClientCertificate? clientCert; - final clientCertFormModel = - form[ClientCertificateFormField.fkClientCertificate] - as ClientCertificateFormModel?; - if (clientCertFormModel != null) { - clientCert = ClientCertificate( - bytes: clientCertFormModel.bytes, - passphrase: clientCertFormModel.passphrase, - ); - } - final credentials = - form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; - try { - await widget.onSubmit( + serverUrl: serverUrl, + clientCertificate: clientCertificate, + ); + // Show onboarding after first login! + final globalSettings = + Hive.box(HiveBoxes.globalSettings).getValue()!; + if (globalSettings.showOnboarding) { + Navigator.push( context, - credentials.username!, - credentials.password!, - form[ServerAddressFormField.fkServerAddress], - clientCert, - ); - } on PaperlessApiException catch (error) { - showErrorMessage(context, error); - } on ServerMessageException catch (error) { - showLocalizedError(context, error.message); - } catch (error) { - showGenericError(context, error); + MaterialPageRoute( + builder: (context) => const ApplicationIntroSlideshow(), + fullscreenDialog: true, + ), + ).then((value) { + globalSettings.showOnboarding = false; + globalSettings.save(); + }); } + // DocumentsRoute().go(context); + } on PaperlessApiException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } on PaperlessFormValidationException catch (exception, stackTrace) { + if (exception.hasUnspecificErrorMessage()) { + showLocalizedError(context, exception.unspecificErrorMessage()!); + } else { + showGenericError( + context, + exception.validationMessages.values.first, + stackTrace, + ); //TODO: Check if we can show error message directly on field here. + } + } catch (unknownError, stackTrace) { + showGenericError(context, unknownError.toString(), stackTrace); } } } diff --git a/lib/features/saved_view_details/view/saved_view_details_page.dart b/lib/features/saved_view_details/view/saved_view_details_page.dart index d82bd21..352e7a2 100644 --- a/lib/features/saved_view_details/view/saved_view_details_page.dart +++ b/lib/features/saved_view_details/view/saved_view_details_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/navigation/push_routes.dart'; @@ -9,6 +10,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/selection/confi import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class SavedViewDetailsPage extends StatefulWidget { final Future Function(SavedView savedView) onDelete; @@ -28,7 +30,7 @@ class _SavedViewDetailsPageState extends State @override Widget build(BuildContext context) { - final cubit = context.read(); + final cubit = context.watch(); return Scaffold( appBar: AppBar( title: Text(cubit.savedView.name), @@ -76,11 +78,10 @@ class _SavedViewDetailsPageState extends State isLoading: state.isLoading, hasLoaded: state.hasLoaded, onTap: (document) { - pushDocumentDetailsRoute( - context, - document: document, + DocumentDetailsRoute( + $extra: document, isLabelClickable: false, - ); + ).push(context); }, viewType: state.viewType, ), diff --git a/lib/features/settings/view/manage_accounts_page.dart b/lib/features/settings/view/manage_accounts_page.dart index 3592144..1d41262 100644 --- a/lib/features/settings/view/manage_accounts_page.dart +++ b/lib/features/settings/view/manage_accounts_page.dart @@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; -import 'package:paperless_mobile/features/login/view/login_page.dart'; +import 'package:paperless_mobile/features/login/view/add_account_page.dart'; import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; @@ -22,7 +22,7 @@ class ManageAccountsPage extends StatelessWidget { builder: (context, globalSettings) { // This is one of the few places where the currentLoggedInUser can be null // (exactly after loggin out as the current user to be precise). - if (globalSettings.currentLoggedInUser == null) { + if (globalSettings.loggedInUserId == null) { return const SizedBox.shrink(); } return ValueListenableBuilder( @@ -32,8 +32,7 @@ class ManageAccountsPage extends StatelessWidget { builder: (context, box, _) { final userIds = box.keys.toList().cast(); final otherAccounts = userIds - .whereNot( - (element) => element == globalSettings.currentLoggedInUser) + .whereNot((element) => element == globalSettings.loggedInUserId) .toList(); return SimpleDialog( insetPadding: const EdgeInsets.all(24), @@ -54,7 +53,7 @@ class ManageAccountsPage extends StatelessWidget { children: [ Card( child: UserAccountListTile( - account: box.get(globalSettings.currentLoggedInUser!)!, + account: box.get(globalSettings.loggedInUserId!)!, trailing: PopupMenuButton( icon: const Icon(Icons.more_vert), itemBuilder: (context) => [ @@ -71,8 +70,7 @@ class ManageAccountsPage extends StatelessWidget { ], onSelected: (value) async { if (value == 0) { - final currentUser = - globalSettings.currentLoggedInUser!; + final currentUser = globalSettings.loggedInUserId!; await context.read().logout(); Navigator.of(context).pop(); await context @@ -117,7 +115,7 @@ class ManageAccountsPage extends StatelessWidget { // Switch _onSwitchAccount( context, - globalSettings.currentLoggedInUser!, + globalSettings.loggedInUserId!, otherAccounts[index], ); } else if (value == 1) { @@ -135,10 +133,10 @@ class ManageAccountsPage extends StatelessWidget { title: Text(S.of(context)!.addAccount), leading: const Icon(Icons.person_add), onTap: () { - _onAddAccount(context, globalSettings.currentLoggedInUser!); + _onAddAccount(context, globalSettings.loggedInUserId!); }, ), - if (context.watch().hasMultiUserSupport) + if (context.watch().hasMultiUserSupport) ListTile( leading: const Icon(Icons.admin_panel_settings), title: Text(S.of(context)!.managePermissions), @@ -155,7 +153,7 @@ class ManageAccountsPage extends StatelessWidget { final userId = await Navigator.push( context, MaterialPageRoute( - builder: (context) => LoginPage( + builder: (context) => AddAccountPage( titleString: S.of(context)!.addAccount, onSubmit: (context, username, password, serverUrl, clientCertificate) async { diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/view/settings_page.dart index 2740172..a0d5ce4 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/view/settings_page.dart @@ -100,14 +100,4 @@ class SettingsPage extends StatelessWidget { ), ); } - - void _goto(Widget page, BuildContext context) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => page, - maintainState: true, - ), - ); - } } diff --git a/lib/features/settings/view/widgets/user_settings_builder.dart b/lib/features/settings/view/widgets/user_settings_builder.dart index 6512efe..2201a58 100644 --- a/lib/features/settings/view/widgets/user_settings_builder.dart +++ b/lib/features/settings/view/widgets/user_settings_builder.dart @@ -23,7 +23,7 @@ class UserAccountBuilder extends StatelessWidget { builder: (context, accountBox, _) { final currentUser = Hive.box(HiveBoxes.globalSettings) .getValue()! - .currentLoggedInUser; + .loggedInUserId; if (currentUser != null) { final account = accountBox.get(currentUser); return builder(context, account); diff --git a/lib/features/similar_documents/view/similar_documents_view.dart b/lib/features/similar_documents/view/similar_documents_view.dart index 35564c4..3219fe7 100644 --- a/lib/features/similar_documents/view/similar_documents_view.dart +++ b/lib/features/similar_documents/view/similar_documents_view.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/navigation/push_routes.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; class SimilarDocumentsView extends StatefulWidget { final ScrollController pagingScrollController; @@ -64,11 +64,10 @@ class _SimilarDocumentsViewState extends State hasLoaded: state.hasLoaded, enableHeroAnimation: false, onTap: (document) { - pushDocumentDetailsRoute( - context, - document: document, + DocumentDetailsRoute( + $extra: document, isLabelClickable: false, - ); + ).push(context); }, ); }, diff --git a/lib/main.dart b/lib/main.dart index d86dd73..2eebcf1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:go_router/go_router.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -30,19 +31,23 @@ import 'package:paperless_mobile/core/interceptor/language_header.interceptor.da import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; -import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart'; -import 'package:paperless_mobile/features/home/view/home_route.dart'; -import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; -import 'package:paperless_mobile/features/login/model/client_certificate.dart'; -import 'package:paperless_mobile/features/login/model/login_form_credentials.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart'; -import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; -import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart'; import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; -import 'package:paperless_mobile/helpers/message_helpers.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; +import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart'; +import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; +import 'package:paperless_mobile/routes/typed/branches/landing_route.dart'; +import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart'; +import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart'; +import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart'; +import 'package:paperless_mobile/routes/typed/top_level/login_route.dart'; +import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart'; +import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart'; +import 'package:paperless_mobile/routes/typed/top_level/verify_identity_route.dart'; import 'package:paperless_mobile/theme.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; @@ -138,7 +143,9 @@ void main() async { }); final apiFactory = PaperlessApiFactoryImpl(sessionManager); - + final authenticationCubit = + AuthenticationCubit(localAuthService, apiFactory, sessionManager); + await authenticationCubit.restoreSessionState(); runApp( MultiProvider( providers: [ @@ -154,13 +161,10 @@ void main() async { child: MultiBlocProvider( providers: [ BlocProvider.value(value: connectivityCubit), - BlocProvider( - create: (context) => AuthenticationCubit( - localAuthService, apiFactory, sessionManager), - ), + BlocProvider.value(value: authenticationCubit), ], - child: PaperlessMobileEntrypoint( - paperlessProviderFactory: apiFactory, + child: GoRouterShell( + apiFactory: apiFactory, ), ), ), @@ -182,70 +186,69 @@ void main() async { }); } -class PaperlessMobileEntrypoint extends StatefulWidget { - final PaperlessApiFactory paperlessProviderFactory; - const PaperlessMobileEntrypoint({ - Key? key, - required this.paperlessProviderFactory, - }) : super(key: key); +// class PaperlessMobileEntrypoint extends StatefulWidget { +// final PaperlessApiFactory paperlessProviderFactory; +// const PaperlessMobileEntrypoint({ +// Key? key, +// required this.paperlessProviderFactory, +// }) : super(key: key); + +// @override +// State createState() => +// _PaperlessMobileEntrypointState(); +// } + +// class _PaperlessMobileEntrypointState extends State { +// @override +// Widget build(BuildContext context) { +// return GlobalSettingsBuilder( +// builder: (context, settings) { +// return DynamicColorBuilder( +// builder: (lightDynamic, darkDynamic) { +// return MaterialApp( +// debugShowCheckedModeBanner: true, +// title: "Paperless Mobile", +// theme: buildTheme( +// brightness: Brightness.light, +// dynamicScheme: lightDynamic, +// preferredColorScheme: settings.preferredColorSchemeOption, +// ), +// darkTheme: buildTheme( +// brightness: Brightness.dark, +// dynamicScheme: darkDynamic, +// preferredColorScheme: settings.preferredColorSchemeOption, +// ), +// themeMode: settings.preferredThemeMode, +// supportedLocales: S.supportedLocales, +// locale: Locale.fromSubtags( +// languageCode: settings.preferredLocaleSubtag, +// ), +// localizationsDelegates: const [ +// ...S.localizationsDelegates, +// ], +// home: AuthenticationWrapper( +// paperlessProviderFactory: widget.paperlessProviderFactory, +// ), +// ); +// }, +// ); +// }, +// ); +// } +// } + +class GoRouterShell extends StatefulWidget { + final PaperlessApiFactory apiFactory; + const GoRouterShell({ + super.key, + required this.apiFactory, + }); @override - State createState() => - _PaperlessMobileEntrypointState(); + State createState() => _GoRouterShellState(); } -class _PaperlessMobileEntrypointState extends State { - @override - Widget build(BuildContext context) { - return GlobalSettingsBuilder( - builder: (context, settings) { - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - return MaterialApp( - debugShowCheckedModeBanner: true, - title: "Paperless Mobile", - theme: buildTheme( - brightness: Brightness.light, - dynamicScheme: lightDynamic, - preferredColorScheme: settings.preferredColorSchemeOption, - ), - darkTheme: buildTheme( - brightness: Brightness.dark, - dynamicScheme: darkDynamic, - preferredColorScheme: settings.preferredColorSchemeOption, - ), - themeMode: settings.preferredThemeMode, - supportedLocales: S.supportedLocales, - locale: Locale.fromSubtags( - languageCode: settings.preferredLocaleSubtag, - ), - localizationsDelegates: const [ - ...S.localizationsDelegates, - ], - home: AuthenticationWrapper( - paperlessProviderFactory: widget.paperlessProviderFactory, - ), - ); - }, - ); - }, - ); - } -} - -class AuthenticationWrapper extends StatefulWidget { - final PaperlessApiFactory paperlessProviderFactory; - - const AuthenticationWrapper({ - Key? key, - required this.paperlessProviderFactory, - }) : super(key: key); - - @override - State createState() => _AuthenticationWrapperState(); -} - -class _AuthenticationWrapperState extends State { +class _GoRouterShellState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); @@ -257,7 +260,6 @@ class _AuthenticationWrapperState extends State { @override void initState() { super.initState(); - // Activate the highest supported refresh rate on the device if (Platform.isAndroid) { _setOptimalDisplayMode(); @@ -281,75 +283,180 @@ class _AuthenticationWrapperState extends State { await FlutterDisplayMode.setPreferredMode(mostOptimalMode); } + late final _router = GoRouter( + debugLogDiagnostics: true, + initialLocation: "/login", + routes: [ + $loginRoute, + $verifyIdentityRoute, + $switchingAccountsRoute, + $settingsRoute, + ShellRoute( + navigatorKey: rootNavigatorKey, + builder: ProviderShellRoute(widget.apiFactory).build, + routes: [ + // GoRoute( + // parentNavigatorKey: rootNavigatorKey, + // name: R.savedView, + // path: "/saved_view/:id", + // builder: (context, state) { + // return Placeholder( + // child: Text("Documents"), + // ); + // }, + // routes: [ + // GoRoute( + // path: "create", + // name: R.createSavedView, + // builder: (context, state) { + // return Placeholder( + // child: Text("Documents"), + // ); + // }, + // ), + // ], + // ), + StatefulShellRoute.indexedStack( + builder: const ScaffoldShellRoute().builder, + branches: [ + StatefulShellBranch( + navigatorKey: landingNavigatorKey, + routes: [$landingRoute], + ), + StatefulShellBranch( + navigatorKey: documentsNavigatorKey, + routes: [$documentsRoute], + ), + StatefulShellBranch( + navigatorKey: scannerNavigatorKey, + routes: [$scannerRoute], + ), + StatefulShellBranch( + navigatorKey: labelsNavigatorKey, + routes: [$labelsRoute], + ), + StatefulShellBranch( + navigatorKey: inboxNavigatorKey, + routes: [$inboxRoute], + ), + ], + ), + ], + ), + ], + ); + @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, authentication) { - return authentication.when( - unauthenticated: () => LoginPage( - titleString: S.of(context)!.connectToPaperless, - submitText: S.of(context)!.signIn, - onSubmit: _onLogin, - showLocalAccounts: true, - ), - requriresLocalAuthentication: () => const VerifyIdentityPage(), - authenticated: (localUserId, apiVersion) => HomeRoute( - key: ValueKey(localUserId), - paperlessApiVersion: apiVersion, - paperlessProviderFactory: widget.paperlessProviderFactory, - localUserId: localUserId, - ), - switchingAccounts: () => const SwitchingAccountsPage(), + return GlobalSettingsBuilder( + builder: (context, settings) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + return BlocListener( + listener: (context, state) { + state.when( + unauthenticated: () => const LoginRoute().go(context), + requriresLocalAuthentication: () => + const VerifyIdentityRoute().go(context), + authenticated: (localUserId) => + const LandingRoute().go(context), + switchingAccounts: () => + const SwitchingAccountsRoute().go(context), + ); + }, + child: MaterialApp.router( + routerConfig: _router, + debugShowCheckedModeBanner: true, + title: "Paperless Mobile", + theme: buildTheme( + brightness: Brightness.light, + dynamicScheme: lightDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, + ), + darkTheme: buildTheme( + brightness: Brightness.dark, + dynamicScheme: darkDynamic, + preferredColorScheme: settings.preferredColorSchemeOption, + ), + themeMode: settings.preferredThemeMode, + supportedLocales: S.supportedLocales, + locale: Locale.fromSubtags( + languageCode: settings.preferredLocaleSubtag, + ), + localizationsDelegates: S.localizationsDelegates, + ), + ); + }, ); }, ); } - - void _onLogin( - BuildContext context, - String username, - String password, - String serverUrl, - ClientCertificate? clientCertificate, - ) async { - try { - await context.read().login( - credentials: LoginFormCredentials( - username: username, - password: password, - ), - serverUrl: serverUrl, - clientCertificate: clientCertificate, - ); - // Show onboarding after first login! - final globalSettings = - Hive.box(HiveBoxes.globalSettings).getValue()!; - if (globalSettings.showOnboarding) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ApplicationIntroSlideshow(), - fullscreenDialog: true, - ), - ).then((value) { - globalSettings.showOnboarding = false; - globalSettings.save(); - }); - } - } on PaperlessApiException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } on PaperlessFormValidationException catch (exception, stackTrace) { - if (exception.hasUnspecificErrorMessage()) { - showLocalizedError(context, exception.unspecificErrorMessage()!); - } else { - showGenericError( - context, - exception.validationMessages.values.first, - stackTrace, - ); //TODO: Check if we can show error message directly on field here. - } - } catch (unknownError, stackTrace) { - showGenericError(context, unknownError.toString(), stackTrace); - } - } } + +// class AuthenticationWrapper extends StatefulWidget { +// final PaperlessApiFactory paperlessProviderFactory; + +// const AuthenticationWrapper({ +// Key? key, +// required this.paperlessProviderFactory, +// }) : super(key: key); + +// @override +// State createState() => _AuthenticationWrapperState(); +// } + +// class _AuthenticationWrapperState extends State { +// @override +// void didChangeDependencies() { +// super.didChangeDependencies(); +// context.read().restoreSessionState().then((value) { +// FlutterNativeSplash.remove(); +// }); +// } + +// @override +// void initState() { +// super.initState(); + +// // Activate the highest supported refresh rate on the device +// if (Platform.isAndroid) { +// _setOptimalDisplayMode(); +// } +// initializeDateFormatting(); +// } + +// Future _setOptimalDisplayMode() async { +// final List supported = await FlutterDisplayMode.supported; +// final DisplayMode active = await FlutterDisplayMode.active; + +// final List sameResolution = supported +// .where((m) => m.width == active.width && m.height == active.height) +// .toList() +// ..sort((a, b) => b.refreshRate.compareTo(a.refreshRate)); + +// final DisplayMode mostOptimalMode = +// sameResolution.isNotEmpty ? sameResolution.first : active; +// debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}'); + +// await FlutterDisplayMode.setPreferredMode(mostOptimalMode); +// } + +// @override +// Widget build(BuildContext context) { +// return BlocBuilder( +// builder: (context, authentication) { +// return authentication.when( +// unauthenticated: () => const LoginPage(), +// requriresLocalAuthentication: () => const VerifyIdentityPage(), +// authenticated: (localUserId, apiVersion) => HomeShellWidget( +// key: ValueKey(localUserId), +// paperlessApiVersion: apiVersion, +// paperlessProviderFactory: widget.paperlessProviderFactory, +// localUserId: localUserId, +// ), +// switchingAccounts: () => const SwitchingAccountsPage(), +// ); +// }, +// ); +// } +// } diff --git a/lib/routes/document_details_route.dart b/lib/routes/document_details_route.dart deleted file mode 100644 index a90001e..0000000 --- a/lib/routes/document_details_route.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; -import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; - -class DocumentDetailsRoute extends StatelessWidget { - final DocumentModel document; - final bool isLabelClickable; - final String? titleAndContentQueryString; - - const DocumentDetailsRoute({ - super.key, - required this.document, - this.isLabelClickable = true, - this.titleAndContentQueryString, - }); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => DocumentDetailsCubit( - context.read(), - context.read(), - context.read(), - context.read(), - initialDocument: document, - ), - lazy: false, - child: DocumentDetailsPage( - isLabelClickable: isLabelClickable, - titleAndContentQueryString: titleAndContentQueryString, - ), - ); - } -} - -class DocumentDetailsRouteArguments { - final DocumentModel document; - final bool isLabelClickable; - final bool allowEdit; - final String? titleAndContentQueryString; - - DocumentDetailsRouteArguments({ - required this.document, - this.isLabelClickable = true, - this.allowEdit = true, - this.titleAndContentQueryString, - }); -} diff --git a/lib/routes/navigation_keys.dart b/lib/routes/navigation_keys.dart new file mode 100644 index 0000000..fc6cbd4 --- /dev/null +++ b/lib/routes/navigation_keys.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +final rootNavigatorKey = GlobalKey(); +final landingNavigatorKey = GlobalKey(); +final documentsNavigatorKey = GlobalKey(); +final scannerNavigatorKey = GlobalKey(); +final labelsNavigatorKey = GlobalKey(); +final inboxNavigatorKey = GlobalKey(); diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart new file mode 100644 index 0000000..291ff43 --- /dev/null +++ b/lib/routes/routes.dart @@ -0,0 +1,20 @@ +class R { + const R._(); + static const landing = "landing"; + static const login = "login"; + static const documents = "documents"; + static const verifyIdentity = "verifyIdentity"; + static const switchingAccounts = "switchingAccounts"; + static const savedView = "savedView"; + static const createSavedView = "createSavedView"; + static const documentDetails = "documentDetails"; + static const editDocument = "editDocument"; + static const labels = "labels"; + static const createLabel = "createLabel"; + static const editLabel = "editLabel"; + static const scanner = "scanner"; + static const uploadDocument = "upload"; + static const inbox = "inbox"; + static const documentPreview = "documentPreview"; + static const settings = "settings"; +} diff --git a/lib/routes/typed/branches/documents_route.dart b/lib/routes/typed/branches/documents_route.dart new file mode 100644 index 0000000..6174838 --- /dev/null +++ b/lib/routes/typed/branches/documents_route.dart @@ -0,0 +1,113 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; +import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; +import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart'; +import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart'; +import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; +import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'documents_route.g.dart'; + +class DocumentsBranch extends StatefulShellBranchData { + static final GlobalKey $navigatorKey = documentsNavigatorKey; + const DocumentsBranch(); +} + +@TypedGoRoute( + path: "/documents", + name: R.documents, + routes: [ + TypedGoRoute( + path: "edit", + name: R.editDocument, + ), + TypedGoRoute( + path: "details", + name: R.documentDetails, + ), + TypedGoRoute( + path: "preview", + name: R.documentPreview, + ) + ], +) +class DocumentsRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const DocumentsPage(); + } +} + +class DocumentDetailsRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final bool isLabelClickable; + final DocumentModel $extra; + final String? queryString; + + const DocumentDetailsRoute({ + required this.$extra, + this.isLabelClickable = true, + this.queryString, + }); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BlocProvider( + create: (_) => DocumentDetailsCubit( + context.read(), + context.read(), + context.read(), + context.read(), + initialDocument: $extra, + ), + lazy: false, + child: DocumentDetailsPage( + isLabelClickable: isLabelClickable, + titleAndContentQueryString: queryString, + ), + ); + } +} + +class EditDocumentRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final DocumentModel $extra; + + const EditDocumentRoute(this.$extra); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BlocProvider( + create: (context) => DocumentEditCubit( + context.read(), + context.read(), + context.read(), + document: $extra, + )..loadFieldSuggestions(), + child: const DocumentEditPage(), + ); + } +} + +class DocumentPreviewRoute extends GoRouteData { + final DocumentModel $extra; + const DocumentPreviewRoute(this.$extra); + + @override + Widget build(BuildContext context, GoRouterState state) { + return DocumentView( + documentBytes: context.read().download($extra), + ); + } +} diff --git a/lib/routes/typed/branches/inbox_route.dart b/lib/routes/typed/branches/inbox_route.dart new file mode 100644 index 0000000..48f038f --- /dev/null +++ b/lib/routes/typed/branches/inbox_route.dart @@ -0,0 +1,17 @@ +import 'package:flutter/src/widgets/framework.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'inbox_route.g.dart'; + +@TypedGoRoute( + path: "/inbox", + name: R.inbox, +) +class InboxRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const InboxPage(); + } +} diff --git a/lib/routes/typed/branches/labels_route.dart b/lib/routes/typed/branches/labels_route.dart new file mode 100644 index 0000000..e1deac8 --- /dev/null +++ b/lib/routes/typed/branches/labels_route.dart @@ -0,0 +1,84 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart'; +import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart'; +import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'labels_route.g.dart'; + +class LabelsBranch extends StatefulShellBranchData { + static final GlobalKey $navigatorKey = labelsNavigatorKey; + const LabelsBranch(); +} + +@TypedGoRoute( + path: "/labels", + name: R.labels, + routes: [ + TypedGoRoute( + path: "edit", + name: R.editLabel, + ), + TypedGoRoute( + path: "create", + name: R.createLabel, + ), + ], +) +class LabelsRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const LabelsPage(); + } +} + +class EditLabelRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final Label $extra; + + const EditLabelRoute(this.$extra); + + @override + Widget build(BuildContext context, GoRouterState state) { + return switch ($extra) { + Correspondent c => EditCorrespondentPage(correspondent: c), + DocumentType d => EditDocumentTypePage(documentType: d), + Tag t => EditTagPage(tag: t), + StoragePath s => EditStoragePathPage(storagePath: s), + }; + } +} + +class CreateLabelRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + final String? name; + + CreateLabelRoute({ + this.name, + }); + + @override + Widget build(BuildContext context, GoRouterState state) { + if (T is Correspondent) { + return AddCorrespondentPage(initialName: name); + } else if (T is DocumentType) { + return AddDocumentTypePage(initialName: name); + } else if (T is Tag) { + return AddTagPage(initialName: name); + } else if (T is StoragePath) { + return AddStoragePathPage(initialName: name); + } + throw ArgumentError(); + } +} diff --git a/lib/routes/typed/branches/landing_route.dart b/lib/routes/typed/branches/landing_route.dart new file mode 100644 index 0000000..78c58ba --- /dev/null +++ b/lib/routes/typed/branches/landing_route.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/landing/view/landing_page.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'landing_route.g.dart'; + +class LandingBranch extends StatefulShellBranchData { + static final GlobalKey $navigatorKey = landingNavigatorKey; + + const LandingBranch(); +} + +@TypedGoRoute( + path: "/landing", + name: R.landing, + routes: [ + TypedGoRoute( + path: "saved-view", + name: R.savedView, + ), + ], +) +class LandingRoute extends GoRouteData { + const LandingRoute(); + @override + Widget build(BuildContext context, GoRouterState state) { + return const LandingPage(); + } +} + +class SavedViewRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return Placeholder(); + } +} diff --git a/lib/routes/typed/branches/scanner_route.dart b/lib/routes/typed/branches/scanner_route.dart new file mode 100644 index 0000000..4bfb01d --- /dev/null +++ b/lib/routes/typed/branches/scanner_route.dart @@ -0,0 +1,82 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/document_scan/view/scanner_page.dart'; +import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart'; +import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'scanner_route.g.dart'; + +// @TypedStatefulShellBranch( +// routes: [ +// TypedGoRoute( +// path: "/scanner", +// name: R.scanner, +// routes: [ +// TypedGoRoute( +// path: "upload", +// name: R.uploadDocument, +// ), +// ], +// ), +// ], +// ) +class ScannerBranch extends StatefulShellBranchData { + static final GlobalKey $navigatorKey = scannerNavigatorKey; + + const ScannerBranch(); +} + +@TypedGoRoute( + path: "/scanner", + name: R.scanner, + routes: [ + TypedGoRoute( + path: "upload", + name: R.uploadDocument, + ), + ], +) +class ScannerRoute extends GoRouteData { + const ScannerRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ScannerPage(); + } +} + +class DocumentUploadRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + final Uint8List $extra; + final String? title; + final String? filename; + final String? fileExtension; + + const DocumentUploadRoute({ + required this.$extra, + this.title, + this.filename, + this.fileExtension, + }); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BlocProvider( + create: (_) => DocumentUploadCubit( + context.read(), + context.read(), + context.read(), + ), + child: DocumentUploadPreparationPage( + title: title, + fileExtension: fileExtension, + filename: filename, + fileBytes: $extra, + ), + ); + } +} diff --git a/lib/routes/typed/shells/provider_shell_route.dart b/lib/routes/typed/shells/provider_shell_route.dart new file mode 100644 index 0000000..99f29a4 --- /dev/null +++ b/lib/routes/typed/shells/provider_shell_route.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; +import 'package:paperless_mobile/features/home/view/home_shell_widget.dart'; +import 'package:paperless_mobile/routes/navigation_keys.dart'; + +//part 'provider_shell_route.g.dart'; +//TODO: Wait for https://github.com/flutter/flutter/issues/127371 to be merged +// @TypedShellRoute( +// routes: [ +// TypedStatefulShellRoute( +// branches: [ +// TypedStatefulShellBranch( +// routes: [ +// TypedGoRoute( +// path: "/landing", +// // name: R.landing, +// ) +// ], +// ), +// TypedStatefulShellBranch( +// routes: [ +// TypedGoRoute( +// path: "/documents", +// routes: [ +// TypedGoRoute( +// path: "details", +// // name: R.documentDetails, +// ), +// TypedGoRoute( +// path: "edit", +// // name: R.editDocument, +// ), +// ], +// ) +// ], +// ), +// ], +// ), +// ], +// ) +class ProviderShellRoute extends ShellRouteData { + final PaperlessApiFactory apiFactory; + static final GlobalKey $navigatorKey = rootNavigatorKey; + + const ProviderShellRoute(this.apiFactory); + + Widget build( + BuildContext context, + GoRouterState state, + Widget navigator, + ) { + final currentUserId = Hive.box(HiveBoxes.globalSettings) + .getValue()! + .loggedInUserId!; + final authenticatedUser = + Hive.box(HiveBoxes.localUserAccount).get( + currentUserId, + )!; + return HomeShellWidget( + localUserId: authenticatedUser.id, + paperlessApiVersion: authenticatedUser.apiVersion, + paperlessProviderFactory: apiFactory, + child: navigator, + ); + } +} diff --git a/lib/routes/typed/shells/scaffold_shell_route.dart b/lib/routes/typed/shells/scaffold_shell_route.dart new file mode 100644 index 0000000..cd86589 --- /dev/null +++ b/lib/routes/typed/shells/scaffold_shell_route.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive/hive.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; +import 'package:paperless_mobile/core/database/tables/global_settings.dart'; +import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/features/home/view/scaffold_with_navigation_bar.dart'; + +class ScaffoldShellRoute extends StatefulShellRouteData { + const ScaffoldShellRoute(); + @override + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) { + final currentUserId = Hive.box(HiveBoxes.globalSettings) + .getValue()! + .loggedInUserId!; + final authenticatedUser = + Hive.box(HiveBoxes.localUserAccount).get( + currentUserId, + )!; + return ScaffoldWithNavigationBar( + authenticatedUser: authenticatedUser.paperlessUser, + navigationShell: navigationShell, + ); + } +} diff --git a/lib/routes/typed/top_level/login_route.dart b/lib/routes/typed/top_level/login_route.dart new file mode 100644 index 0000000..ce6cc8f --- /dev/null +++ b/lib/routes/typed/top_level/login_route.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/view/login_page.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'login_route.g.dart'; + +@TypedGoRoute( + path: "/login", + name: R.login, +) +class LoginRoute extends GoRouteData { + const LoginRoute(); + @override + Widget build(BuildContext context, GoRouterState state) { + return const LoginPage(); + } + + @override + FutureOr redirect(BuildContext context, GoRouterState state) { + if (context.read().state.isAuthenticated) { + return "/landing"; + } + return null; + } +} diff --git a/lib/routes/typed/top_level/settings_route.dart b/lib/routes/typed/top_level/settings_route.dart new file mode 100644 index 0000000..0b0664c --- /dev/null +++ b/lib/routes/typed/top_level/settings_route.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/settings/view/settings_page.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'settings_route.g.dart'; + +@TypedGoRoute( + path: "/settings", + name: R.settings, +) +class SettingsRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const SettingsPage(); + } +} diff --git a/lib/routes/typed/top_level/switching_accounts_route.dart b/lib/routes/typed/top_level/switching_accounts_route.dart new file mode 100644 index 0000000..1dc74dc --- /dev/null +++ b/lib/routes/typed/top_level/switching_accounts_route.dart @@ -0,0 +1,18 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'switching_accounts_route.g.dart'; + +@TypedGoRoute( + path: '/switching-accounts', + name: R.switchingAccounts, +) +class SwitchingAccountsRoute extends GoRouteData { + const SwitchingAccountsRoute(); + @override + Widget build(BuildContext context, GoRouterState state) { + return const SwitchingAccountsPage(); + } +} diff --git a/lib/routes/typed/top_level/verify_identity_route.dart b/lib/routes/typed/top_level/verify_identity_route.dart new file mode 100644 index 0000000..5e62dd1 --- /dev/null +++ b/lib/routes/typed/top_level/verify_identity_route.dart @@ -0,0 +1,19 @@ +import 'package:go_router/go_router.dart'; +import 'package:flutter/widgets.dart'; +import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart'; +import 'package:paperless_mobile/routes/routes.dart'; + +part 'verify_identity_route.g.dart'; + +@TypedGoRoute( + path: '/verify-identity', + name: R.verifyIdentity, +) +class VerifyIdentityRoute extends GoRouteData { + const VerifyIdentityRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const VerifyIdentityPage(); + } +} diff --git a/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart b/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart index 51cc066..1cad789 100644 --- a/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart +++ b/packages/paperless_api/lib/src/models/exception/paperless_server_message_exception.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -part 'paperless_server_exception.g.dart'; +part 'paperless_server_message_exception.g.dart'; @JsonSerializable(createToJson: false) class PaperlessServerMessageException implements Exception { @@ -13,5 +13,5 @@ class PaperlessServerMessageException implements Exception { } factory PaperlessServerMessageException.fromJson(Map json) => - _$PaperlessServerExceptionFromJson(json); + _$PaperlessServerMessageExceptionFromJson(json); } diff --git a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart b/packages/paperless_api/lib/src/models/labels/correspondent_model.dart deleted file mode 100644 index 7a97202..0000000 --- a/packages/paperless_api/lib/src/models/labels/correspondent_model.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; -import 'package:paperless_api/src/models/labels/label_model.dart'; -import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; - -part 'correspondent_model.g.dart'; - -@LocalDateTimeJsonConverter() -@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) -class Correspondent extends Label { - final DateTime? lastCorrespondence; - - const Correspondent({ - this.lastCorrespondence, - required super.name, - super.id, - super.slug, - super.match, - super.matchingAlgorithm, - super.isInsensitive, - super.documentCount, - super.owner, - super.userCanChange, - }); - - factory Correspondent.fromJson(Map json) => - _$CorrespondentFromJson(json); - - @override - Map toJson() => _$CorrespondentToJson(this); - - @override - String toString() { - return name; - } - - @override - Correspondent copyWith({ - int? id, - String? name, - String? slug, - String? match, - MatchingAlgorithm? matchingAlgorithm, - bool? isInsensitive, - int? documentCount, - DateTime? lastCorrespondence, - }) { - return Correspondent( - id: id ?? this.id, - name: name ?? this.name, - documentCount: documentCount ?? documentCount, - isInsensitive: isInsensitive ?? isInsensitive, - match: match ?? this.match, - matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, - slug: slug ?? this.slug, - lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence, - ); - } - - @override - String get queryEndpoint => 'correspondents'; - - @override - List get props => [ - id, - name, - slug, - isInsensitive, - documentCount, - lastCorrespondence, - matchingAlgorithm, - match, - ]; -} diff --git a/packages/paperless_api/lib/src/models/labels/document_type_model.dart b/packages/paperless_api/lib/src/models/labels/document_type_model.dart deleted file mode 100644 index efd7285..0000000 --- a/packages/paperless_api/lib/src/models/labels/document_type_model.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/models/labels/label_model.dart'; -import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; -part 'document_type_model.g.dart'; - -@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) -class DocumentType extends Label { - const DocumentType({ - super.id, - required super.name, - super.slug, - super.match, - super.matchingAlgorithm, - super.isInsensitive, - super.documentCount, - super.owner, - super.userCanChange, - }); - - factory DocumentType.fromJson(Map json) => _$DocumentTypeFromJson(json); - - @override - String get queryEndpoint => 'document_types'; - - @override - DocumentType copyWith({ - int? id, - String? name, - String? match, - MatchingAlgorithm? matchingAlgorithm, - bool? isInsensitive, - int? documentCount, - String? slug, - }) { - return DocumentType( - id: id ?? this.id, - name: name ?? this.name, - match: match ?? this.match, - matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, - isInsensitive: isInsensitive ?? this.isInsensitive, - documentCount: documentCount ?? this.documentCount, - slug: slug ?? this.slug, - ); - } - - @override - Map toJson() => _$DocumentTypeToJson(this); - - @override - List get props => [ - id, - name, - slug, - isInsensitive, - documentCount, - matchingAlgorithm, - match, - ]; -} diff --git a/packages/paperless_api/lib/src/models/labels/label_model.dart b/packages/paperless_api/lib/src/models/labels/label_model.dart index ee9c2b8..5970696 100644 --- a/packages/paperless_api/lib/src/models/labels/label_model.dart +++ b/packages/paperless_api/lib/src/models/labels/label_model.dart @@ -1,7 +1,14 @@ +import 'dart:ui'; + import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:paperless_api/src/converters/hex_color_json_converter.dart'; +import 'package:paperless_api/src/converters/local_date_time_json_converter.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; -abstract class Label extends Equatable implements Comparable { +part 'label_model.g.dart'; + +sealed class Label extends Equatable implements Comparable { static const idKey = "id"; static const nameKey = "name"; static const slugKey = "slug"; @@ -56,3 +63,278 @@ abstract class Label extends Equatable implements Comparable { Map toJson(); } + +@LocalDateTimeJsonConverter() +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) +class Correspondent extends Label { + final DateTime? lastCorrespondence; + + const Correspondent({ + this.lastCorrespondence, + required super.name, + super.id, + super.slug, + super.match, + super.matchingAlgorithm, + super.isInsensitive, + super.documentCount, + super.owner, + super.userCanChange, + }); + + factory Correspondent.fromJson(Map json) => + _$CorrespondentFromJson(json); + + @override + Map toJson() => _$CorrespondentToJson(this); + + @override + String toString() { + return name; + } + + @override + Correspondent copyWith({ + int? id, + String? name, + String? slug, + String? match, + MatchingAlgorithm? matchingAlgorithm, + bool? isInsensitive, + int? documentCount, + DateTime? lastCorrespondence, + }) { + return Correspondent( + id: id ?? this.id, + name: name ?? this.name, + documentCount: documentCount ?? documentCount, + isInsensitive: isInsensitive ?? isInsensitive, + match: match ?? this.match, + matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, + slug: slug ?? this.slug, + lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence, + ); + } + + @override + String get queryEndpoint => 'correspondents'; + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + lastCorrespondence, + matchingAlgorithm, + match, + ]; +} + +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) +class DocumentType extends Label { + const DocumentType({ + super.id, + required super.name, + super.slug, + super.match, + super.matchingAlgorithm, + super.isInsensitive, + super.documentCount, + super.owner, + super.userCanChange, + }); + + factory DocumentType.fromJson(Map json) => + _$DocumentTypeFromJson(json); + + @override + String get queryEndpoint => 'document_types'; + + @override + DocumentType copyWith({ + int? id, + String? name, + String? match, + MatchingAlgorithm? matchingAlgorithm, + bool? isInsensitive, + int? documentCount, + String? slug, + }) { + return DocumentType( + id: id ?? this.id, + name: name ?? this.name, + match: match ?? this.match, + matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, + isInsensitive: isInsensitive ?? this.isInsensitive, + documentCount: documentCount ?? this.documentCount, + slug: slug ?? this.slug, + ); + } + + @override + Map toJson() => _$DocumentTypeToJson(this); + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + matchingAlgorithm, + match, + ]; +} + +@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) +class StoragePath extends Label { + static const pathKey = 'path'; + final String path; + + const StoragePath({ + super.id, + required super.name, + required this.path, + super.slug, + super.match, + super.matchingAlgorithm, + super.isInsensitive, + super.documentCount, + super.owner, + super.userCanChange, + }); + + factory StoragePath.fromJson(Map json) => + _$StoragePathFromJson(json); + + @override + String toString() { + return name; + } + + @override + StoragePath copyWith({ + int? id, + String? name, + String? slug, + String? match, + MatchingAlgorithm? matchingAlgorithm, + bool? isInsensitive, + int? documentCount, + String? path, + }) { + return StoragePath( + id: id ?? this.id, + name: name ?? this.name, + documentCount: documentCount ?? documentCount, + isInsensitive: isInsensitive ?? isInsensitive, + path: path ?? this.path, + match: match ?? this.match, + matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, + slug: slug ?? this.slug, + ); + } + + @override + String get queryEndpoint => 'storage_paths'; + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + path, + matchingAlgorithm, + match, + ]; + + @override + Map toJson() => _$StoragePathToJson(this); +} + +@HexColorJsonConverter() +@JsonSerializable( + fieldRename: FieldRename.snake, + explicitToJson: true, +) +class Tag extends Label { + static const colorKey = 'color'; + static const isInboxTagKey = 'is_inbox_tag'; + static const textColorKey = 'text_color'; + static const legacyColourKey = 'colour'; + final Color? textColor; + final Color? color; + + final bool isInboxTag; + + const Tag({ + super.id, + required super.name, + super.documentCount, + super.isInsensitive, + super.match, + super.matchingAlgorithm = MatchingAlgorithm.defaultValue, + super.slug, + this.color, + this.textColor, + this.isInboxTag = false, + super.owner, + super.userCanChange, + }); + + @override + String toString() => name; + + @override + Tag copyWith({ + int? id, + String? name, + String? match, + MatchingAlgorithm? matchingAlgorithm, + bool? isInsensitive, + int? documentCount, + String? slug, + Color? color, + Color? textColor, + bool? isInboxTag, + }) { + return Tag( + id: id ?? this.id, + name: name ?? this.name, + match: match ?? this.match, + matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, + isInsensitive: isInsensitive ?? this.isInsensitive, + documentCount: documentCount ?? this.documentCount, + slug: slug ?? this.slug, + color: color ?? this.color, + textColor: textColor ?? this.textColor, + isInboxTag: isInboxTag ?? this.isInboxTag, + ); + } + + @override + String get queryEndpoint => 'tags'; + + @override + List get props => [ + id, + name, + slug, + isInsensitive, + documentCount, + matchingAlgorithm, + color, + textColor, + isInboxTag, + match, + ]; + + factory Tag.fromJson(Map json) => _$TagFromJson(json); + + @override + Map toJson() => _$TagToJson(this); +} diff --git a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart b/packages/paperless_api/lib/src/models/labels/storage_path_model.dart deleted file mode 100644 index b05e53e..0000000 --- a/packages/paperless_api/lib/src/models/labels/storage_path_model.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/models/labels/label_model.dart'; -import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; -part 'storage_path_model.g.dart'; - -@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake) -class StoragePath extends Label { - static const pathKey = 'path'; - final String path; - - const StoragePath({ - super.id, - required super.name, - required this.path, - super.slug, - super.match, - super.matchingAlgorithm, - super.isInsensitive, - super.documentCount, - super.owner, - super.userCanChange, - }); - - factory StoragePath.fromJson(Map json) => _$StoragePathFromJson(json); - - @override - String toString() { - return name; - } - - @override - StoragePath copyWith({ - int? id, - String? name, - String? slug, - String? match, - MatchingAlgorithm? matchingAlgorithm, - bool? isInsensitive, - int? documentCount, - String? path, - }) { - return StoragePath( - id: id ?? this.id, - name: name ?? this.name, - documentCount: documentCount ?? documentCount, - isInsensitive: isInsensitive ?? isInsensitive, - path: path ?? this.path, - match: match ?? this.match, - matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, - slug: slug ?? this.slug, - ); - } - - @override - String get queryEndpoint => 'storage_paths'; - - @override - List get props => [ - id, - name, - slug, - isInsensitive, - documentCount, - path, - matchingAlgorithm, - match, - ]; - - @override - Map toJson() => _$StoragePathToJson(this); -} diff --git a/packages/paperless_api/lib/src/models/labels/tag_model.dart b/packages/paperless_api/lib/src/models/labels/tag_model.dart deleted file mode 100644 index 6052809..0000000 --- a/packages/paperless_api/lib/src/models/labels/tag_model.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'dart:ui'; - -import 'package:json_annotation/json_annotation.dart'; -import 'package:paperless_api/src/converters/hex_color_json_converter.dart'; -import 'package:paperless_api/src/models/labels/label_model.dart'; -import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; - -part 'tag_model.g.dart'; - -@HexColorJsonConverter() -@JsonSerializable( - fieldRename: FieldRename.snake, - explicitToJson: true, -) -class Tag extends Label { - static const colorKey = 'color'; - static const isInboxTagKey = 'is_inbox_tag'; - static const textColorKey = 'text_color'; - static const legacyColourKey = 'colour'; - final Color? textColor; - final Color? color; - - final bool isInboxTag; - - const Tag({ - super.id, - required super.name, - super.documentCount, - super.isInsensitive, - super.match, - super.matchingAlgorithm = MatchingAlgorithm.defaultValue, - super.slug, - this.color, - this.textColor, - this.isInboxTag = false, - super.owner, - super.userCanChange, - }); - - @override - String toString() => name; - - @override - Tag copyWith({ - int? id, - String? name, - String? match, - MatchingAlgorithm? matchingAlgorithm, - bool? isInsensitive, - int? documentCount, - String? slug, - Color? color, - Color? textColor, - bool? isInboxTag, - }) { - return Tag( - id: id ?? this.id, - name: name ?? this.name, - match: match ?? this.match, - matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, - isInsensitive: isInsensitive ?? this.isInsensitive, - documentCount: documentCount ?? this.documentCount, - slug: slug ?? this.slug, - color: color ?? this.color, - textColor: textColor ?? this.textColor, - isInboxTag: isInboxTag ?? this.isInboxTag, - ); - } - - @override - String get queryEndpoint => 'tags'; - - @override - List get props => [ - id, - name, - slug, - isInsensitive, - documentCount, - matchingAlgorithm, - color, - textColor, - isInboxTag, - match, - ]; - - factory Tag.fromJson(Map json) => _$TagFromJson(json); - - @override - Map toJson() => _$TagToJson(this); -} diff --git a/packages/paperless_api/lib/src/models/models.dart b/packages/paperless_api/lib/src/models/models.dart index d9ebaa8..e5b8495 100644 --- a/packages/paperless_api/lib/src/models/models.dart +++ b/packages/paperless_api/lib/src/models/models.dart @@ -5,12 +5,8 @@ export 'document_model.dart'; export 'field_suggestions.dart'; export 'filter_rule_model.dart'; export 'group_model.dart'; -export 'labels/correspondent_model.dart'; -export 'labels/document_type_model.dart'; export 'labels/label_model.dart'; export 'labels/matching_algorithm.dart'; -export 'labels/storage_path_model.dart'; -export 'labels/tag_model.dart'; export 'paged_search_result.dart'; export 'paperless_api_exception.dart'; export 'paperless_server_information_model.dart'; diff --git a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart index 87b4935..0a6921f 100644 --- a/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart +++ b/packages/paperless_api/lib/src/models/permissions/user_permission_extension.dart @@ -81,4 +81,10 @@ extension UserPermissionExtension on UserModel { hasPermission(PermissionAction.add, PermissionTarget.storagePath); bool get canCreateSavedViews => hasPermission(PermissionAction.add, PermissionTarget.savedView); + + bool get canViewAnyLabel => + canViewCorrespondents || + canViewDocumentTypes || + canViewTags || + canViewStoragePaths; } diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart index b373e55..eb5f543 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api.dart @@ -1,7 +1,5 @@ -import 'package:paperless_api/src/models/labels/correspondent_model.dart'; -import 'package:paperless_api/src/models/labels/document_type_model.dart'; -import 'package:paperless_api/src/models/labels/storage_path_model.dart'; -import 'package:paperless_api/src/models/labels/tag_model.dart'; + +import 'package:paperless_api/src/models/models.dart'; /// /// Provides basic CRUD operations for labels, including: diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index e45dbf8..740e713 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -3,10 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:paperless_api/src/extensions/dio_exception_extension.dart'; -import 'package:paperless_api/src/models/labels/correspondent_model.dart'; -import 'package:paperless_api/src/models/labels/document_type_model.dart'; -import 'package:paperless_api/src/models/labels/storage_path_model.dart'; -import 'package:paperless_api/src/models/labels/tag_model.dart'; +import 'package:paperless_api/src/models/models.dart'; import 'package:paperless_api/src/models/paperless_api_exception.dart'; import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart'; import 'package:paperless_api/src/request_utils.dart'; diff --git a/pubspec.lock b/pubspec.lock index 1673463..7c38544 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -317,10 +317,10 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: "1dc1fa763b73ed52147bd91b015d81903edc3f227b77b1672fcddba43390ed18" + sha256: "3dede3f7abc077a4181ec7445448a289a9ce08e2981e6a4d49a3fb5099d47e1f" url: "https://pub.dev" source: hosted - version: "5.7.5" + version: "5.7.6" dart_code_metrics_presets: dependency: transitive description: @@ -381,18 +381,18 @@ packages: dependency: "direct main" description: name: dio - sha256: a9d76e72985d7087eb7c5e7903224ae52b337131518d127c554b9405936752b8 + sha256: "3866d67f93523161b643187af65f5ac08bc991a5bcdaf41a2d587fe4ccb49993" url: "https://pub.dev" source: hosted - version: "5.2.1+1" + version: "5.3.0" dots_indicator: dependency: transitive description: name: dots_indicator - sha256: "58b6a365744aa62aa1b70c4ea29e5106fbe064f5edaf7e9652e9b856edbfd9bb" + sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.1.2" dynamic_color: dependency: "direct main" description: @@ -727,18 +727,18 @@ packages: dependency: "direct dev" description: name: freezed - sha256: a9520490532087cf38bf3f7de478ab6ebeb5f68bb1eb2641546d92719b224445 + sha256: "2df89855fe181baae3b6d714dc3c4317acf4fccd495a6f36e5e00f24144c6c3b" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -760,6 +760,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + go_router_builder: + dependency: "direct dev" + description: + name: go_router_builder + sha256: df2034629637d0c7c380aba5daa2f91be4733f2d632e7dff0b082d5ff3155068 + url: "https://pub.dev" + source: hosted + version: "2.2.4" graphs: dependency: transitive description: @@ -865,10 +881,10 @@ packages: dependency: "direct main" description: name: introduction_screen - sha256: f39be426026785b8fea4ed93e226e7fc28ef49a4c78c3f86c958bae26dabef00 + sha256: ef5a5479a8e06a84b9a7eff16c698b9b82f70cd1b6203b264bc3686f9bfb77e2 url: "https://pub.dev" source: hosted - version: "3.1.9" + version: "3.1.11" io: dependency: transitive description: @@ -1143,10 +1159,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" path_provider_linux: dependency: transitive description: @@ -1183,34 +1199,34 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "415af30ba76a84faccfe1eb251fe1e4fdc790f876924c65ad7d6ed7a1404bcd6" + sha256: "63e5216aae014a72fe9579ccd027323395ce7a98271d9defa9d57320d001af81" url: "https://pub.dev" source: hosted - version: "10.4.2" + version: "10.4.3" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "3b61f3da3b1c83bc3fb6a2b431e8dab01d0e5b45f6a3d9c7609770ec88b2a89e" + sha256: "2ffaf52a21f64ac9b35fe7369bb9533edbd4f698e5604db8645b1064ff4cf221" url: "https://pub.dev" source: hosted - version: "10.3.0" + version: "10.3.3" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "7a187b671a39919462af2b5e813148365b71a615979165a119868d667fe90c03" + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" url: "https://pub.dev" source: hosted - version: "9.1.3" + version: "9.1.4" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "463a07cb7cc6c758a7a1c7da36ce666bb80a0b4b5e92df0fa36872e0ed456993" + sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" url: "https://pub.dev" source: hosted - version: "3.11.1" + version: "3.11.3" permission_handler_windows: dependency: transitive description: @@ -1247,10 +1263,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pointer_interceptor: dependency: transitive description: @@ -1468,10 +1484,10 @@ packages: dependency: "direct main" description: name: sliver_tools - sha256: ccdc502098a8bfa07b3ec582c282620031481300035584e1bb3aca296a505e8c + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 url: "https://pub.dev" source: hosted - version: "0.2.10" + version: "0.2.12" source_gen: dependency: transitive description: @@ -1516,18 +1532,18 @@ packages: dependency: transitive description: name: sqflite - sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 + sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" url: "https://pub.dev" source: hosted - version: "2.2.8+4" + version: "2.3.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" + sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" url: "https://pub.dev" source: hosted - version: "2.4.5+1" + version: "2.5.0" stack_trace: dependency: transitive description: @@ -1652,10 +1668,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af" url: "https://pub.dev" source: hosted - version: "6.0.36" + version: "6.0.37" url_launcher_ios: dependency: transitive description: @@ -1676,10 +1692,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_platform_interface: dependency: transitive description: @@ -1772,26 +1788,26 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "1c93e96f3069bacdc734fad6b7e1d3a480fd516a3ae5b8858becf7f07515a2f3" + sha256: d936a09fbfd08cb78f7329e0bbacf6158fbdfe24ffc908b22444c07d295eb193 url: "https://pub.dev" source: hosted - version: "3.8.2" + version: "3.9.2" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "656e2aeaef318900fffd21468b6ddc7958c7092a642f0e7220bac328b70d4a81" + sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: a8d7e8b4be2a79e83b70235369971ec97d14df4cdbb40d305a8eeae67d8e6432 + sha256: "5fa098f28b606f699e8ca52d9e4e11edbbfef65189f5f77ae92703ba5408fd25" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3b44c50..cba6496 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: webview_flutter: ^4.2.1 printing: ^5.11.0 flutter_pdfview: ^1.3.1 + go_router: ^10.0.0 dependency_overrides: intl: ^0.18.1 @@ -113,6 +114,7 @@ dev_dependencies: hive_generator: ^2.0.0 mock_server: path: packages/mock_server + go_router_builder: ^2.2.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec