diff --git a/lib/core/navigation/push_routes.dart b/lib/core/navigation/push_routes.dart index 77e5312..e1d9fe2 100644 --- a/lib/core/navigation/push_routes.dart +++ b/lib/core/navigation/push_routes.dart @@ -1,15 +1,8 @@ -import 'dart:typed_data'; - -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'; @@ -18,14 +11,7 @@ import 'package:paperless_mobile/core/repository/user_repository.dart'; import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart'; -import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; -import 'package:paperless_mobile/features/document_search/view/document_search_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/features/home/view/model/api_version.dart'; -import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart'; -import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart'; -import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart'; @@ -33,29 +19,6 @@ import 'package:paperless_mobile/features/saved_view_details/view/saved_view_det import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -// These are convenience methods for nativating to views without having to pass providers around explicitly. -// Providers unfortunately have to be passed to the routes since they are children of the Navigator, not ancestors. - -Future pushDocumentSearchPage(BuildContext context) { - final currentUser = Hive.box(HiveBoxes.globalSettings) - .getValue()! - .loggedInUserId; - final userRepo = context.read(); - return Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BlocProvider( - create: (context) => DocumentSearchCubit( - context.read(), - context.read(), - Hive.box(HiveBoxes.localUserAppState) - .get(currentUser)!, - ), - child: const DocumentSearchPage(), - ), - ), - ); -} - Future pushSavedViewDetailsRoute( BuildContext context, { required SavedView savedView, @@ -84,7 +47,8 @@ Future pushSavedViewDetailsRoute( savedView: savedView, ), child: SavedViewDetailsPage( - onDelete: context.read().remove), + onDelete: context.read().remove, + ), ); }, ), @@ -107,39 +71,6 @@ Future pushAddSavedViewRoute(BuildContext context, ); } -Future pushLinkedDocumentsView( - BuildContext context, { - required DocumentFilter filter, -}) { - return Navigator.push( - context, - 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.watch().hasMultiUserSupport) - Provider.value(value: context.read()), - ], - builder: (context, _) => BlocProvider( - create: (context) => LinkedDocumentsCubit( - filter, - context.read(), - context.read(), - context.read(), - ), - child: const LinkedDocumentsPage(), - ), - ), - ), - ); -} - Future pushBulkEditCorrespondentRoute( BuildContext context, { required List selection, @@ -306,44 +237,6 @@ Future pushBulkEditDocumentTypeRoute(BuildContext context, ); } -Future pushDocumentUploadPreparationPage( - BuildContext context, { - required Uint8List bytes, - String? filename, - String? fileExtension, - String? title, -}) { - final labelRepo = context.read(); - final docsApi = context.read(); - final connectivity = context.read(); - final apiVersion = context.read(); - return Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => MultiProvider( - providers: [ - Provider.value(value: labelRepo), - Provider.value(value: docsApi), - Provider.value(value: connectivity), - Provider.value(value: apiVersion) - ], - builder: (_, child) => BlocProvider( - create: (_) => DocumentUploadCubit( - context.read(), - context.read(), - context.read(), - ), - child: DocumentUploadPreparationPage( - fileBytes: bytes, - fileExtension: fileExtension, - filename: filename, - title: title, - ), - ), - ), - ), - ); -} - List _getRequiredBulkEditProviders(BuildContext context) { return [ Provider.value(value: context.read()), diff --git a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart index 2d2a7a6..33dee5e 100644 --- a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart +++ b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; @@ -86,6 +87,7 @@ class _FullscreenBulkEditLabelPageState selectionCount: _labels.length, floatingActionButton: !hideFab ? FloatingActionButton.extended( + heroTag: "fab_fullscreen_bulk_edit_label", onPressed: _onSubmit, label: Text(S.of(context)!.apply), icon: const Icon(Icons.done), @@ -122,7 +124,7 @@ class _FullscreenBulkEditLabelPageState void _onSubmit() async { if (_selection == null) { - Navigator.pop(context); + context.pop(); } else { bool shouldPerformAction; if (_selection!.label == null) { @@ -148,7 +150,7 @@ class _FullscreenBulkEditLabelPageState } if (shouldPerformAction) { widget.onSubmit(_selection!.label); - Navigator.pop(context); + context.pop(); } } } diff --git a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart index 1248521..dbeb107 100644 --- a/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart +++ b/lib/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.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/widgets/form_fields/fullscreen_selection_form.dart'; import 'package:paperless_mobile/extensions/dart_extensions.dart'; @@ -74,6 +75,7 @@ class _FullscreenBulkEditTagsWidgetState controller: _controller, floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty ? FloatingActionButton.extended( + heroTag: "fab_fullscreen_bulk_edit_tags", label: Text(S.of(context)!.apply), icon: const Icon(Icons.done), onPressed: _submit, @@ -173,7 +175,7 @@ class _FullscreenBulkEditTagsWidgetState removeTagIds: _removeTags, addTagIds: _addTags, ); - Navigator.pop(context); + context.pop(); } } } 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 79216fb..4f4249e 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_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:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; @@ -92,37 +93,43 @@ class _DocumentDetailsPageState extends State { DocumentDetailsState>( builder: (context, state) { return Positioned.fill( - child: DocumentPreview( - document: state.document, - fit: BoxFit.cover, + child: GestureDetector( + onTap: () { + DocumentPreviewRoute($extra: state.document) + .push(context); + }, + child: DocumentPreview( + document: state.document, + fit: BoxFit.cover, + ), ), ); }, ), - Positioned.fill( - top: 0, - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Theme.of(context) - .colorScheme - .background - .withOpacity(0.8), - Theme.of(context) - .colorScheme - .background - .withOpacity(0.5), - Colors.transparent, - Colors.transparent, - Colors.transparent, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - ), - ), + // Positioned.fill( + // top: -kToolbarHeight, + // child: DecoratedBox( + // decoration: BoxDecoration( + // gradient: LinearGradient( + // colors: [ + // Theme.of(context) + // .colorScheme + // .background + // .withOpacity(0.8), + // Theme.of(context) + // .colorScheme + // .background + // .withOpacity(0.5), + // Colors.transparent, + // Colors.transparent, + // Colors.transparent, + // ], + // begin: Alignment.topCenter, + // end: Alignment.bottomCenter, + // ), + // ), + // ), + // ), ], ), ), @@ -302,6 +309,7 @@ class _DocumentDetailsPageState extends State { preferBelow: false, verticalOffset: 40, child: FloatingActionButton( + heroTag: "fab_document_details", child: const Icon(Icons.edit), onPressed: () => EditDocumentRoute(state.document).push(context), ), @@ -333,13 +341,13 @@ class _DocumentDetailsPageState extends State { document: state.document, enabled: isConnected, ), - //TODO: Enable again, need new pdf viewer package... - IconButton( - tooltip: S.of(context)!.previewTooltip, - icon: const Icon(Icons.visibility), - onPressed: - (isConnected) ? () => _onOpen(state.document) : null, - ).paddedOnly(right: 4.0), + // //TODO: Enable again, need new pdf viewer package... + // IconButton( + // tooltip: S.of(context)!.previewTooltip, + // icon: const Icon(Icons.visibility), + // onPressed: + // (isConnected) ? () => _onOpen(state.document) : null, + // ).paddedOnly(right: 4.0), IconButton( tooltip: S.of(context)!.openInSystemViewer, icon: const Icon(Icons.open_in_new), @@ -391,21 +399,17 @@ class _DocumentDetailsPageState extends State { } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { - // Document deleted => go back to primary route - Navigator.popUntil(context, (route) => route.isFirst); + do { + context.pop(); + } while (context.canPop()); } } } Future _onOpen(DocumentModel document) async { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => DocumentView( - documentBytes: - context.read().download(document), - title: document.title, - ), - ), - ); + DocumentPreviewRoute( + $extra: document, + title: document.title, + ).push(context); } } diff --git a/lib/features/document_details/view/widgets/document_meta_data_widget.dart b/lib/features/document_details/view/widgets/document_meta_data_widget.dart index 8eae4f8..bdfcf81 100644 --- a/lib/features/document_details/view/widgets/document_meta_data_widget.dart +++ b/lib/features/document_details/view/widgets/document_meta_data_widget.dart @@ -2,6 +2,7 @@ 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'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart'; @@ -25,6 +26,7 @@ class DocumentMetaDataWidget extends StatefulWidget { class _DocumentMetaDataWidgetState extends State { @override Widget build(BuildContext context) { + final currentUser = context.watch().paperlessUser; return BlocBuilder( builder: (context, state) { if (state.metaData == null) { @@ -37,9 +39,10 @@ class _DocumentMetaDataWidgetState extends State { return SliverList( delegate: SliverChildListDelegate( [ - ArchiveSerialNumberField( - document: widget.document, - ).paddedOnly(bottom: widget.itemSpacing), + if (currentUser.canEditDocuments) + ArchiveSerialNumberField( + document: widget.document, + ).paddedOnly(bottom: widget.itemSpacing), DetailsItem.text( DateFormat().format(widget.document.modified), context: context, 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 b19b49a..f38d266 100644 --- a/lib/features/document_details/view/widgets/document_overview_widget.dart +++ b/lib/features/document_details/view/widgets/document_overview_widget.dart @@ -31,71 +31,66 @@ class DocumentOverviewWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildListDelegate( - [ + return SliverList.list( + children: [ + DetailsItem( + label: S.of(context)!.title, + content: HighlightedText( + text: document.title, + highlights: queryString?.split(" ") ?? [], + style: Theme.of(context).textTheme.bodyLarge, + ), + ).paddedOnly(bottom: itemSpacing), + DetailsItem.text( + DateFormat.yMMMMd().format(document.created), + context: context, + label: S.of(context)!.createdAt, + ).paddedOnly(bottom: itemSpacing), + if (document.documentType != null && + context + .watch() + .paperlessUser + .canViewDocumentTypes) DetailsItem( - label: S.of(context)!.title, - content: HighlightedText( - text: document.title, - highlights: queryString?.split(" ") ?? [], + label: S.of(context)!.documentType, + content: LabelText( style: Theme.of(context).textTheme.bodyLarge, + label: availableDocumentTypes[document.documentType], ), ).paddedOnly(bottom: itemSpacing), - DetailsItem.text( - DateFormat.yMMMMd().format(document.created), - context: context, - label: S.of(context)!.createdAt, + if (document.correspondent != null && + context + .watch() + .paperlessUser + .canViewCorrespondents) + DetailsItem( + label: S.of(context)!.correspondent, + content: LabelText( + style: Theme.of(context).textTheme.bodyLarge, + label: availableCorrespondents[document.correspondent], + ), ).paddedOnly(bottom: itemSpacing), - if (document.documentType != null && - context - .watch() - .paperlessUser - .canViewDocumentTypes) - DetailsItem( - label: S.of(context)!.documentType, - content: LabelText( - style: Theme.of(context).textTheme.bodyLarge, - label: availableDocumentTypes[document.documentType], + if (document.storagePath != null && + context.watch().paperlessUser.canViewStoragePaths) + DetailsItem( + label: S.of(context)!.storagePath, + content: LabelText( + label: availableStoragePaths[document.storagePath], + ), + ).paddedOnly(bottom: itemSpacing), + if (document.tags.isNotEmpty && + context.watch().paperlessUser.canViewTags) + DetailsItem( + label: S.of(context)!.tags, + content: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TagsWidget( + isClickable: false, + tags: document.tags.map((e) => availableTags[e]!).toList(), ), - ).paddedOnly(bottom: itemSpacing), - if (document.correspondent != null && - context - .watch() - .paperlessUser - .canViewCorrespondents) - DetailsItem( - label: S.of(context)!.correspondent, - content: LabelText( - style: Theme.of(context).textTheme.bodyLarge, - label: availableCorrespondents[document.correspondent], - ), - ).paddedOnly(bottom: itemSpacing), - if (document.storagePath != null && - context - .watch() - .paperlessUser - .canViewStoragePaths) - DetailsItem( - label: S.of(context)!.storagePath, - content: LabelText( - label: availableStoragePaths[document.storagePath], - ), - ).paddedOnly(bottom: itemSpacing), - if (document.tags.isNotEmpty && - context.watch().paperlessUser.canViewTags) - DetailsItem( - label: S.of(context)!.tags, - content: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: TagsWidget( - isClickable: false, - tags: document.tags.map((e) => availableTags[e]!).toList(), - ), - ), - ).paddedOnly(bottom: itemSpacing), - ], - ), + ), + ).paddedOnly(bottom: itemSpacing), + ], ); } } diff --git a/lib/features/document_edit/view/document_edit_page.dart b/lib/features/document_edit/view/document_edit_page.dart index 8cb014d..8453c24 100644 --- a/lib/features/document_edit/view/document_edit_page.dart +++ b/lib/features/document_edit/view/document_edit_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -44,6 +45,7 @@ class _DocumentEditPageState extends State { @override Widget build(BuildContext context) { + final currentUser = context.watch().paperlessUser; return BlocBuilder( builder: (context, state) { final filteredSuggestions = state.suggestions?.documentDifference( @@ -53,6 +55,7 @@ class _DocumentEditPageState extends State { child: Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton.extended( + heroTag: "fab_document_edit", onPressed: () => _onSubmit(state.document), icon: const Icon(Icons.save), label: Text(S.of(context)!.saveChanges), @@ -90,147 +93,146 @@ class _DocumentEditPageState extends State { filteredSuggestions, ).padded(), // Correspondent form field - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialValue) => - RepositoryProvider.value( - value: context.read(), - child: AddCorrespondentPage( - initialName: initialValue, - ), - ), - addLabelText: S.of(context)!.addCorrespondent, - labelText: S.of(context)!.correspondent, - options: context - .watch() - .state - .correspondents, - initialValue: - state.document.correspondent != null - ? IdQueryParameter.fromId( - state.document.correspondent!) - : const IdQueryParameter.unset(), - name: fkCorrespondent, - prefixIcon: const Icon(Icons.person_outlined), - allowSelectUnassigned: true, - canCreateNewLabel: context - .watch() - .paperlessUser - .canCreateCorrespondents, - ), - if (filteredSuggestions - ?.hasSuggestedCorrespondents ?? - false) - _buildSuggestionsSkeleton( - suggestions: - filteredSuggestions!.correspondents, - itemBuilder: (context, itemData) => - ActionChip( - label: Text( - state.correspondents[itemData]!.name), - onPressed: () { - _formKey - .currentState?.fields[fkCorrespondent] - ?.didChange( - IdQueryParameter.fromId(itemData), - ); - }, - ), - ), - ], - ).padded(), - // DocumentType form field - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (currentInput) => - RepositoryProvider.value( - value: context.read(), - child: AddDocumentTypePage( - initialName: currentInput, - ), - ), - canCreateNewLabel: context - .watch() - .paperlessUser - .canCreateDocumentTypes, - addLabelText: S.of(context)!.addDocumentType, - labelText: S.of(context)!.documentType, - initialValue: - state.document.documentType != null - ? IdQueryParameter.fromId( - state.document.documentType!) - : const IdQueryParameter.unset(), - options: state.documentTypes, - name: _DocumentEditPageState.fkDocumentType, - prefixIcon: - const Icon(Icons.description_outlined), - allowSelectUnassigned: true, - ), - if (filteredSuggestions - ?.hasSuggestedDocumentTypes ?? - false) - _buildSuggestionsSkeleton( - suggestions: - filteredSuggestions!.documentTypes, - itemBuilder: (context, itemData) => - ActionChip( - label: Text( - state.documentTypes[itemData]!.name), - onPressed: () => _formKey - .currentState?.fields[fkDocumentType] - ?.didChange( - IdQueryParameter.fromId(itemData), + if (currentUser.canViewCorrespondents) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialValue) => + RepositoryProvider.value( + value: context.read(), + child: AddCorrespondentPage( + initialName: initialValue, ), ), + addLabelText: S.of(context)!.addCorrespondent, + labelText: S.of(context)!.correspondent, + options: context + .watch() + .state + .correspondents, + initialValue: + state.document.correspondent != null + ? IdQueryParameter.fromId( + state.document.correspondent!) + : const IdQueryParameter.unset(), + name: fkCorrespondent, + prefixIcon: const Icon(Icons.person_outlined), + allowSelectUnassigned: true, + canCreateNewLabel: + currentUser.canCreateCorrespondents, ), - ], - ).padded(), + if (filteredSuggestions + ?.hasSuggestedCorrespondents ?? + false) + _buildSuggestionsSkeleton( + suggestions: + filteredSuggestions!.correspondents, + itemBuilder: (context, itemData) => + ActionChip( + label: Text( + state.correspondents[itemData]!.name), + onPressed: () { + _formKey.currentState + ?.fields[fkCorrespondent] + ?.didChange( + IdQueryParameter.fromId(itemData), + ); + }, + ), + ), + ], + ).padded(), + // DocumentType form field + if (currentUser.canViewDocumentTypes) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (currentInput) => + RepositoryProvider.value( + value: context.read(), + child: AddDocumentTypePage( + initialName: currentInput, + ), + ), + canCreateNewLabel: + currentUser.canCreateDocumentTypes, + addLabelText: S.of(context)!.addDocumentType, + labelText: S.of(context)!.documentType, + initialValue: + state.document.documentType != null + ? IdQueryParameter.fromId( + state.document.documentType!) + : const IdQueryParameter.unset(), + options: state.documentTypes, + name: _DocumentEditPageState.fkDocumentType, + prefixIcon: + const Icon(Icons.description_outlined), + allowSelectUnassigned: true, + ), + if (filteredSuggestions + ?.hasSuggestedDocumentTypes ?? + false) + _buildSuggestionsSkeleton( + suggestions: + filteredSuggestions!.documentTypes, + itemBuilder: (context, itemData) => + ActionChip( + label: Text( + state.documentTypes[itemData]!.name), + onPressed: () => _formKey + .currentState?.fields[fkDocumentType] + ?.didChange( + IdQueryParameter.fromId(itemData), + ), + ), + ), + ], + ).padded(), // StoragePath form field - Column( - children: [ - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialValue) => - RepositoryProvider.value( - value: context.read(), - child: AddStoragePathPage( - initialName: initialValue), + if (currentUser.canViewStoragePaths) + Column( + children: [ + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialValue) => + RepositoryProvider.value( + value: context.read(), + child: AddStoragePathPage( + initialName: initialValue), + ), + canCreateNewLabel: + currentUser.canCreateStoragePaths, + addLabelText: S.of(context)!.addStoragePath, + labelText: S.of(context)!.storagePath, + options: state.storagePaths, + initialValue: + state.document.storagePath != null + ? IdQueryParameter.fromId( + state.document.storagePath!) + : const IdQueryParameter.unset(), + name: fkStoragePath, + prefixIcon: const Icon(Icons.folder_outlined), + allowSelectUnassigned: true, ), - canCreateNewLabel: context - .watch() - .paperlessUser - .canCreateStoragePaths, - addLabelText: S.of(context)!.addStoragePath, - labelText: S.of(context)!.storagePath, - options: state.storagePaths, - initialValue: state.document.storagePath != null - ? IdQueryParameter.fromId( - state.document.storagePath!) - : const IdQueryParameter.unset(), - name: fkStoragePath, - prefixIcon: const Icon(Icons.folder_outlined), - allowSelectUnassigned: true, - ), - ], - ).padded(), + ], + ).padded(), // Tag form field - TagsFormField( - options: state.tags, - name: fkTags, - allowOnlySelection: true, - allowCreation: true, - allowExclude: false, - initialValue: TagsQuery.ids( - include: state.document.tags.toList(), - ), - ).padded(), + if (currentUser.canViewTags) + TagsFormField( + options: state.tags, + name: fkTags, + allowOnlySelection: true, + allowCreation: true, + allowExclude: false, + initialValue: TagsQuery.ids( + include: state.document.tags.toList(), + ), + ).padded(), if (filteredSuggestions?.tags .toSet() .difference(state.document.tags.toSet()) @@ -321,7 +323,7 @@ class _DocumentEditPageState extends State { setState(() { _isSubmitLoading = false; }); - Navigator.pop(context); + context.pop(); } } } diff --git a/lib/features/document_scan/view/scanner_page.dart b/lib/features/document_scan/view/scanner_page.dart index 71aed2b..fca17f9 100644 --- a/lib/features/document_scan/view/scanner_page.dart +++ b/lib/features/document_scan/view/scanner_page.dart @@ -22,11 +22,13 @@ import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_c import 'package:paperless_mobile/features/document_scan/view/widgets/export_scans_dialog.dart'; import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_image_item.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; +import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.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/helpers/permission_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart'; import 'package:path/path.dart' as p; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; @@ -53,58 +55,52 @@ class _ScannerPageState extends State Widget build(BuildContext context) { return BlocBuilder( builder: (context, connectedState) { - return Scaffold( - drawer: const AppDrawer(), - floatingActionButton: FloatingActionButton( - onPressed: () => _openDocumentScanner(context), - child: const Icon(Icons.add_a_photo_outlined), - ), - body: BlocBuilder>( - builder: (context, state) { - return SafeArea( - child: Scaffold( - drawer: const AppDrawer(), - floatingActionButton: FloatingActionButton( - onPressed: () => _openDocumentScanner(context), - child: const Icon(Icons.add_a_photo_outlined), - ), - body: NestedScrollView( - floatHeaderSlivers: true, - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - handle: searchBarHandle, - sliver: SliverSearchBar( - titleText: S.of(context)!.scanner, - ), + return BlocBuilder>( + builder: (context, state) { + return SafeArea( + child: Scaffold( + drawer: const AppDrawer(), + floatingActionButton: FloatingActionButton( + heroTag: "fab_document_edit", + onPressed: () => _openDocumentScanner(context), + child: const Icon(Icons.add_a_photo_outlined), + ), + body: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: searchBarHandle, + sliver: SliverSearchBar( + titleText: S.of(context)!.scanner, ), - SliverOverlapAbsorber( - handle: actionsHandle, - sliver: SliverPinnedHeader( - child: _buildActions(connectedState.isConnected), - ), - ), - ], - body: BlocBuilder>( - builder: (context, state) { - if (state.isEmpty) { - return SizedBox.expand( - child: Center( - child: _buildEmptyState( - connectedState.isConnected, - state, - ), - ), - ); - } else { - return _buildImageGrid(state); - } - }, ), + SliverOverlapAbsorber( + handle: actionsHandle, + sliver: SliverPinnedHeader( + child: _buildActions(connectedState.isConnected), + ), + ), + ], + body: BlocBuilder>( + builder: (context, state) { + if (state.isEmpty) { + return SizedBox.expand( + child: Center( + child: _buildEmptyState( + connectedState.isConnected, + state, + ), + ), + ); + } else { + return _buildImageGrid(state); + } + }, ), ), - ); - }, - ), + ), + ); + }, ); }, ); @@ -260,11 +256,10 @@ class _ScannerPageState extends State .getValue()! .enforceSinglePagePdfUpload, ); - final uploadResult = await pushDocumentUploadPreparationPage( - context, - bytes: file.bytes, + final uploadResult = await DocumentUploadRoute( + $extra: file.bytes, fileExtension: file.extension, - ); + ).push(context); if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) { // For paperless version older than 1.11.3, task id will always be null! context.read().reset(); @@ -366,13 +361,12 @@ class _ScannerPageState extends State ); return; } - pushDocumentUploadPreparationPage( - context, - bytes: file.readAsBytesSync(), + DocumentUploadRoute( + $extra: file.readAsBytesSync(), filename: fileDescription.filename, title: fileDescription.filename, fileExtension: fileDescription.extension, - ); + ).push(context); } } diff --git a/lib/features/document_search/view/document_search_bar.dart b/lib/features/document_search/view/document_search_bar.dart index 08de60e..6ed9eca 100644 --- a/lib/features/document_search/view/document_search_bar.dart +++ b/lib/features/document_search/view/document_search_bar.dart @@ -1,18 +1,12 @@ import 'package:animations/animations.dart'; 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/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/user_repository.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart'; import 'package:paperless_mobile/features/document_search/view/document_search_page.dart'; -import 'package:paperless_mobile/features/home/view/model/api_version.dart'; import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart'; -import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart'; import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -85,24 +79,14 @@ class _DocumentSearchBarState extends State { ); }, openBuilder: (_, action) { - return MultiProvider( - providers: [ - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - Provider.value(value: context.read()), - if (context.watch().hasMultiUserSupport) - Provider.value(value: context.read()), - ], - child: Provider( - create: (_) => DocumentSearchCubit( - context.read(), - context.read(), - Hive.box(HiveBoxes.localUserAppState) - .get(context.watch().id)!, - ), - builder: (_, __) => const DocumentSearchPage(), + return Provider( + create: (_) => DocumentSearchCubit( + context.read(), + context.read(), + Hive.box(HiveBoxes.localUserAppState) + .get(context.read().id)!, ), + child: const DocumentSearchPage(), ); }, ), @@ -114,11 +98,10 @@ class _DocumentSearchBarState extends State { padding: const EdgeInsets.all(6), icon: UserAvatar(account: context.watch()), onPressed: () { - final apiVersion = context.read(); showDialog( context: context, - builder: (context) => Provider.value( - value: apiVersion, + builder: (_) => Provider.value( + value: context.read(), child: const ManageAccountsPage(), ), ); diff --git a/lib/features/document_search/view/sliver_search_bar.dart b/lib/features/document_search/view/sliver_search_bar.dart index 9f8610e..4dcf665 100644 --- a/lib/features/document_search/view/sliver_search_bar.dart +++ b/lib/features/document_search/view/sliver_search_bar.dart @@ -56,11 +56,10 @@ class SliverSearchBar extends StatelessWidget { }, ), onPressed: () { - final apiVersion = context.read(); showDialog( context: context, - builder: (context) => Provider.value( - value: apiVersion, + builder: (_) => Provider.value( + value: context.read(), child: const ManageAccountsPage(), ), ); 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 8990387..0503da4 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -3,8 +3,9 @@ import 'dart:typed_data'; 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/hive.dart'; - +import 'package:image/image.dart' as img; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -56,7 +57,7 @@ class _DocumentUploadPreparationPageState static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss"); final GlobalKey _formKey = GlobalKey(); - + Color? _titleColor; Map _errors = {}; bool _isUploadLoading = false; late bool _syncTitleAndFilename; @@ -67,24 +68,21 @@ class _DocumentUploadPreparationPageState void initState() { super.initState(); _syncTitleAndFilename = widget.filename == null && widget.title == null; + _titleColor = _computeAverageColor().computeLuminance() > 0.5 + ? Colors.black + : Colors.white; initializeDateFormatting(); } @override Widget build(BuildContext context) { return Scaffold( + extendBodyBehindAppBar: false, resizeToAvoidBottomInset: true, - appBar: AppBar( - title: Text(S.of(context)!.prepareDocument), - bottom: _isUploadLoading - ? const PreferredSize( - child: LinearProgressIndicator(), - preferredSize: Size.fromHeight(4.0)) - : null, - ), floatingActionButton: Visibility( visible: MediaQuery.of(context).viewInsets.bottom == 0, child: FloatingActionButton.extended( + heroTag: "fab_document_upload", onPressed: _onSubmit, label: Text(S.of(context)!.upload), icon: const Icon(Icons.upload), @@ -94,183 +92,246 @@ class _DocumentUploadPreparationPageState builder: (context, state) { return FormBuilder( key: _formKey, - child: ListView( - children: [ - // Title - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - name: DocumentModel.titleKey, - initialValue: - widget.title ?? "scan_${fileNameDateFormat.format(_now)}", - validator: (value) { - if (value?.trim().isEmpty ?? true) { - return S.of(context)!.thisFieldIsRequired; - } - return null; - }, - decoration: InputDecoration( - labelText: S.of(context)!.title, - suffixIcon: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _formKey.currentState?.fields[DocumentModel.titleKey] - ?.didChange(""); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName] - ?.didChange(""); - } - }, + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + leading: BackButton( + color: _titleColor, ), - errorText: _errors[DocumentModel.titleKey], - ), - onChanged: (value) { - final String transformedValue = - _formatFilename(value ?? ''); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName] - ?.didChange(transformedValue); - } - }, - ), - // Filename - FormBuilderTextField( - autovalidateMode: AutovalidateMode.always, - readOnly: _syncTitleAndFilename, - enabled: !_syncTitleAndFilename, - name: fkFileName, - decoration: InputDecoration( - labelText: S.of(context)!.fileName, - suffixText: widget.fileExtension, - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () => _formKey.currentState?.fields[fkFileName] - ?.didChange(''), + pinned: true, + expandedHeight: 150, + flexibleSpace: FlexibleSpaceBar( + background: Image.memory( + widget.fileBytes, + fit: BoxFit.cover, + ), + title: Text( + S.of(context)!.prepareDocument, + style: TextStyle( + color: _titleColor, + ), + ), ), - ), - initialValue: widget.filename ?? - "scan_${fileNameDateFormat.format(_now)}", - ), - // Synchronize title and filename - SwitchListTile( - value: _syncTitleAndFilename, - onChanged: (value) { - setState( - () => _syncTitleAndFilename = value, - ); - if (_syncTitleAndFilename) { - final String transformedValue = _formatFilename(_formKey - .currentState - ?.fields[DocumentModel.titleKey] - ?.value as String); - if (_syncTitleAndFilename) { - _formKey.currentState?.fields[fkFileName] - ?.didChange(transformedValue); - } - } - }, - title: Text( - S.of(context)!.synchronizeTitleAndFilename, - ), - ), - // Created at - FormBuilderDateTimePicker( - autovalidateMode: AutovalidateMode.always, - format: DateFormat.yMMMMd(), - inputType: InputType.date, - name: DocumentModel.createdKey, - initialValue: null, - onChanged: (value) { - setState(() => _showDatePickerDeleteIcon = value != null); - }, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.calendar_month_outlined), - labelText: S.of(context)!.createdAt + " *", - suffixIcon: _showDatePickerDeleteIcon - ? IconButton( - icon: const Icon(Icons.close), - onPressed: () { - _formKey.currentState! - .fields[DocumentModel.createdKey] - ?.didChange(null); - }, + bottom: _isUploadLoading + ? const PreferredSize( + child: LinearProgressIndicator(), + preferredSize: Size.fromHeight(4.0), ) : null, ), ), - // Correspondent - if (context - .watch() - .paperlessUser - .canViewCorrespondents) - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialName) => MultiProvider( - providers: [ - Provider.value( - value: context.read(), + ], + body: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Builder( + builder: (context) { + return CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor( + context), ), - Provider.value( - value: context.read(), - ) - ], - child: AddCorrespondentPage(initialName: initialName), - ), - addLabelText: S.of(context)!.addCorrespondent, - labelText: S.of(context)!.correspondent + " *", - name: DocumentModel.correspondentKey, - options: state.correspondents, - prefixIcon: const Icon(Icons.person_outline), - allowSelectUnassigned: true, - canCreateNewLabel: context - .watch() - .paperlessUser - .canCreateCorrespondents, - ), - // Document type - if (context - .watch() - .paperlessUser - .canViewDocumentTypes) - LabelFormField( - showAnyAssignedOption: false, - showNotAssignedOption: false, - addLabelPageBuilder: (initialName) => MultiProvider( - providers: [ - Provider.value( - value: context.read(), + SliverList.list( + children: [ + // Title + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + name: DocumentModel.titleKey, + initialValue: widget.title ?? + "scan_${fileNameDateFormat.format(_now)}", + validator: (value) { + if (value?.trim().isEmpty ?? true) { + return S.of(context)!.thisFieldIsRequired; + } + return null; + }, + decoration: InputDecoration( + labelText: S.of(context)!.title, + suffixIcon: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _formKey.currentState + ?.fields[DocumentModel.titleKey] + ?.didChange(""); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(""); + } + }, + ), + errorText: _errors[DocumentModel.titleKey], + ), + onChanged: (value) { + final String transformedValue = + _formatFilename(value ?? ''); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } + }, + ), + // Filename + FormBuilderTextField( + autovalidateMode: AutovalidateMode.always, + readOnly: _syncTitleAndFilename, + enabled: !_syncTitleAndFilename, + name: fkFileName, + decoration: InputDecoration( + labelText: S.of(context)!.fileName, + suffixText: widget.fileExtension, + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () => _formKey + .currentState?.fields[fkFileName] + ?.didChange(''), + ), + ), + initialValue: widget.filename ?? + "scan_${fileNameDateFormat.format(_now)}", + ), + // Synchronize title and filename + SwitchListTile( + value: _syncTitleAndFilename, + onChanged: (value) { + setState( + () => _syncTitleAndFilename = value, + ); + if (_syncTitleAndFilename) { + final String transformedValue = + _formatFilename(_formKey + .currentState + ?.fields[DocumentModel.titleKey] + ?.value as String); + if (_syncTitleAndFilename) { + _formKey.currentState?.fields[fkFileName] + ?.didChange(transformedValue); + } + } + }, + title: Text( + S.of(context)!.synchronizeTitleAndFilename, + ), + ), + // Created at + FormBuilderDateTimePicker( + autovalidateMode: AutovalidateMode.always, + format: DateFormat.yMMMMd(), + inputType: InputType.date, + name: DocumentModel.createdKey, + initialValue: null, + onChanged: (value) { + setState(() => + _showDatePickerDeleteIcon = value != null); + }, + decoration: InputDecoration( + prefixIcon: + const Icon(Icons.calendar_month_outlined), + labelText: S.of(context)!.createdAt + " *", + suffixIcon: _showDatePickerDeleteIcon + ? IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _formKey.currentState! + .fields[DocumentModel.createdKey] + ?.didChange(null); + }, + ) + : null, + ), + ), + // Correspondent + if (context + .watch() + .paperlessUser + .canViewCorrespondents) + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialName) => + MultiProvider( + providers: [ + Provider.value( + value: context.read(), + ), + Provider.value( + value: context.read(), + ) + ], + child: AddCorrespondentPage( + initialName: initialName), + ), + addLabelText: S.of(context)!.addCorrespondent, + labelText: S.of(context)!.correspondent + " *", + name: DocumentModel.correspondentKey, + options: state.correspondents, + prefixIcon: const Icon(Icons.person_outline), + allowSelectUnassigned: true, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateCorrespondents, + ), + // Document type + if (context + .watch() + .paperlessUser + .canViewDocumentTypes) + LabelFormField( + showAnyAssignedOption: false, + showNotAssignedOption: false, + addLabelPageBuilder: (initialName) => + MultiProvider( + providers: [ + Provider.value( + value: context.read(), + ), + Provider.value( + value: context.read(), + ) + ], + child: AddDocumentTypePage( + initialName: initialName), + ), + addLabelText: S.of(context)!.addDocumentType, + labelText: S.of(context)!.documentType + " *", + name: DocumentModel.documentTypeKey, + options: state.documentTypes, + prefixIcon: + const Icon(Icons.description_outlined), + allowSelectUnassigned: true, + canCreateNewLabel: context + .watch() + .paperlessUser + .canCreateDocumentTypes, + ), + if (context + .watch() + .paperlessUser + .canViewTags) + TagsFormField( + name: DocumentModel.tagsKey, + allowCreation: true, + allowExclude: false, + allowOnlySelection: true, + options: state.tags, + ), + Text( + "* " + S.of(context)!.uploadInferValuesHint, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.justify, + ).padded(), + const SizedBox(height: 300), + ].padded(), ), - Provider.value( - value: context.read(), - ) ], - child: AddDocumentTypePage(initialName: initialName), - ), - addLabelText: S.of(context)!.addDocumentType, - labelText: S.of(context)!.documentType + " *", - name: DocumentModel.documentTypeKey, - options: state.documentTypes, - prefixIcon: const Icon(Icons.description_outlined), - allowSelectUnassigned: true, - canCreateNewLabel: context - .watch() - .paperlessUser - .canCreateDocumentTypes, - ), - if (context.watch().paperlessUser.canViewTags) - TagsFormField( - name: DocumentModel.tagsKey, - allowCreation: true, - allowExclude: false, - allowOnlySelection: true, - options: state.tags, - ), - Text( - "* " + S.of(context)!.uploadInferValuesHint, - style: Theme.of(context).textTheme.bodySmall, + ); + }, ), - const SizedBox(height: 300), - ].padded(), + ), ), ); }, @@ -317,10 +378,7 @@ class _DocumentUploadPreparationPageState context, S.of(context)!.documentSuccessfullyUploadedProcessing, ); - Navigator.pop( - context, - DocumentUploadResult(true, taskId), - ); + context.pop(DocumentUploadResult(true, taskId)); } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } on PaperlessFormValidationException catch (exception) { @@ -345,4 +403,33 @@ class _DocumentUploadPreparationPageState String _formatFilename(String source) { return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase(); } + + Color _computeAverageColor() { + final bitmap = img.decodeImage(widget.fileBytes); + if (bitmap == null) { + return Colors.black; + } + int redBucket = 0; + int greenBucket = 0; + int blueBucket = 0; + int pixelCount = 0; + + for (int y = 0; y < bitmap.height; y++) { + for (int x = 0; x < bitmap.width; x++) { + final c = bitmap.getPixel(x, y); + + pixelCount++; + redBucket += c.r.toInt(); + greenBucket += c.g.toInt(); + blueBucket += c.b.toInt(); + } + } + + return Color.fromRGBO( + redBucket ~/ pixelCount, + greenBucket ~/ pixelCount, + blueBucket ~/ pixelCount, + 1, + ); + } } diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 54444f0..f06cdc8 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -134,7 +134,7 @@ class _DocumentsPageState extends State Padding( padding: const EdgeInsets.all(8.0), child: FloatingActionButton.small( - key: UniqueKey(), + heroTag: "fab_documents_page_reset_filter", backgroundColor: Theme.of(context) .colorScheme .onPrimaryContainer, @@ -164,11 +164,13 @@ class _DocumentsPageState extends State duration: const Duration(milliseconds: 250), child: (_currentTab == 0) ? FloatingActionButton( + heroTag: "fab_documents_page_filter", child: const Icon(Icons.filter_alt_outlined), onPressed: _openDocumentFilter, ) : FloatingActionButton( + heroTag: "fab_documents_page_filter", child: const Icon(Icons.add), onPressed: () => _onCreateSavedView(state.filter), diff --git a/lib/features/documents/view/widgets/document_preview.dart b/lib/features/documents/view/widgets/document_preview.dart index 74da008..576d04d 100644 --- a/lib/features/documents/view/widgets/document_preview.dart +++ b/lib/features/documents/view/widgets/document_preview.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/routes/typed/branches/documents_route.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; @@ -12,6 +13,7 @@ class DocumentPreview extends StatelessWidget { final double borderRadius; final bool enableHero; final double scale; + final bool isClickable; const DocumentPreview({ super.key, @@ -21,15 +23,23 @@ class DocumentPreview extends StatelessWidget { this.borderRadius = 12.0, this.enableHero = true, this.scale = 1.1, + this.isClickable = true, }); @override Widget build(BuildContext context) { - return HeroMode( - enabled: enableHero, - child: Hero( - tag: "thumb_${document.id}", - child: _buildPreview(context), + return GestureDetector( + onTap: isClickable + ? () { + DocumentPreviewRoute($extra: document).push(context); + } + : null, + child: HeroMode( + enabled: enableHero, + child: Hero( + tag: "thumb_${document.id}", + child: _buildPreview(context), + ), ), ); } diff --git a/lib/features/documents/view/widgets/search/document_filter_panel.dart b/lib/features/documents/view/widgets/search/document_filter_panel.dart index e255b6d..8cdb5af 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -80,6 +80,7 @@ class _DocumentFilterPanelState extends State { floatingActionButton: Visibility( visible: MediaQuery.of(context).viewInsets.bottom == 0, child: FloatingActionButton.extended( + heroTag: "fab_document_filter_panel", icon: const Icon(Icons.done), label: Text(S.of(context)!.apply), onPressed: _onApplyFilter, diff --git a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart index 76b4784..52874b9 100644 --- a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart @@ -7,6 +7,7 @@ import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.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 DocumentSelectionSliverAppBar extends StatelessWidget { final DocumentsState state; @@ -65,24 +66,30 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { label: Text(S.of(context)!.correspondent), avatar: const Icon(Icons.edit), onPressed: () { - pushBulkEditCorrespondentRoute(context, - selection: state.selection); + BulkEditDocumentsRoute(BulkEditExtraWrapper( + state.selection, + LabelType.correspondent, + )).push(context); }, ).paddedOnly(left: 8, right: 4), ActionChip( label: Text(S.of(context)!.documentType), avatar: const Icon(Icons.edit), onPressed: () async { - pushBulkEditDocumentTypeRoute(context, - selection: state.selection); + BulkEditDocumentsRoute(BulkEditExtraWrapper( + state.selection, + LabelType.documentType, + )).push(context); }, ).paddedOnly(left: 8, right: 4), ActionChip( label: Text(S.of(context)!.storagePath), avatar: const Icon(Icons.edit), onPressed: () async { - pushBulkEditStoragePathRoute(context, - selection: state.selection); + BulkEditDocumentsRoute(BulkEditExtraWrapper( + state.selection, + LabelType.storagePath, + )).push(context); }, ).paddedOnly(left: 8, right: 4), _buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4), @@ -98,7 +105,10 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { label: Text(S.of(context)!.tags), avatar: const Icon(Icons.edit), onPressed: () { - pushBulkEditTagsRoute(context, selection: state.selection); + BulkEditDocumentsRoute(BulkEditExtraWrapper( + state.selection, + LabelType.tag, + )).push(context); }, ); } diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 7ad57e4..9ed9369 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -2,6 +2,7 @@ import 'dart:developer'; 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/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; @@ -119,11 +120,11 @@ class EditLabelForm extends StatelessWidget { } catch (error, stackTrace) { log("An error occurred!", error: error, stackTrace: stackTrace); } - Navigator.pop(context); + context.pop(); } } else { onDelete(context, label); - Navigator.pop(context); + context.pop(); } } } diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index d0ce3ce..d20c70b 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -1,6 +1,7 @@ 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:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; @@ -74,6 +75,7 @@ class _LabelFormState extends State> { return Scaffold( resizeToAvoidBottomInset: false, floatingActionButton: FloatingActionButton.extended( + heroTag: "fab_label_form", icon: widget.submitButtonConfig.icon, label: widget.submitButtonConfig.label, onPressed: _onSubmit, @@ -168,7 +170,7 @@ class _LabelFormState extends State> { }; final parsed = widget.fromJsonT(mergedJson); final createdLabel = await widget.submitButtonConfig.onSubmit(parsed); - Navigator.pop(context, createdLabel); + context.pop(createdLabel); } on PaperlessApiException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } on PaperlessFormValidationException catch (exception) { diff --git a/lib/features/home/view/home_shell_widget.dart b/lib/features/home/view/home_shell_widget.dart index 69c8a95..f0e0785 100644 --- a/lib/features/home/view/home_shell_widget.dart +++ b/lib/features/home/view/home_shell_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:hive_flutter/adapters.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -16,9 +17,14 @@ 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/login/cubit/authentication_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:paperless_mobile/routes/typed/branches/landing_route.dart'; +import 'package:paperless_mobile/routes/typed/top_level/login_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:provider/provider.dart'; class HomeShellWidget extends StatelessWidget { @@ -57,7 +63,10 @@ class HomeShellWidget extends StatelessWidget { .listenable(keys: [currentUserId]), builder: (context, box, _) { final currentLocalUser = box.get(currentUserId)!; + print(currentLocalUser.paperlessUser.canViewDocuments); + print(currentLocalUser.paperlessUser.canViewTags); return MultiProvider( + key: ValueKey(currentUserId), providers: [ Provider.value(value: currentLocalUser), Provider.value(value: apiVersion), @@ -162,16 +171,22 @@ class HomeShellWidget extends StatelessWidget { create: (context) => DocumentScannerCubit(context.read()), ), - if (currentLocalUser.paperlessUser.canViewDocuments && - currentLocalUser.paperlessUser.canViewTags) - Provider( - create: (context) => InboxCubit( + Provider( + create: (context) { + final inboxCubit = InboxCubit( context.read(), context.read(), context.read(), context.read(), - ).initialize(), - ), + ); + if (currentLocalUser + .paperlessUser.canViewDocuments && + currentLocalUser.paperlessUser.canViewTags) { + inboxCubit.initialize(); + } + return inboxCubit; + }, + ), Provider( create: (context) => SavedViewCubit( context.read(), diff --git a/lib/features/home/view/scaffold_with_navigation_bar.dart b/lib/features/home/view/scaffold_with_navigation_bar.dart index 518b8d3..0d67599 100644 --- a/lib/features/home/view/scaffold_with_navigation_bar.dart +++ b/lib/features/home/view/scaffold_with_navigation_bar.dart @@ -46,24 +46,24 @@ class ScaffoldWithNavigationBarState extends State { if (widget.authenticatedUser.canViewDocuments) { widget.navigationShell.goBranch(index); } else { - showSnackBar( - context, "You do not have permission to access this page."); + showSnackBar(context, + "You do not have the required permissions 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."); + showSnackBar(context, + "You do not have the required permissions 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."); + showSnackBar(context, + "You do not have the required permissions to access this page."); } break; case _inboxIndex: @@ -71,8 +71,8 @@ class ScaffoldWithNavigationBarState extends State { widget.authenticatedUser.canViewTags) { widget.navigationShell.goBranch(index); } else { - showSnackBar( - context, "You do not have permission to access this page."); + showSnackBar(context, + "You do not have the required permissions to access this page."); } break; default: @@ -132,7 +132,7 @@ class ScaffoldWithNavigationBarState extends State { if (!(widget.authenticatedUser.canViewDocuments && widget.authenticatedUser.canViewTags)) { return Icon( - Icons.close, + Icons.inbox_outlined, color: disabledColor, ); } diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 5ee407d..fecaf54 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -48,6 +48,7 @@ class _InboxPageState extends State return const SizedBox.shrink(); } return FloatingActionButton.extended( + heroTag: "fab_inbox", label: Text(S.of(context)!.allSeen), icon: const Icon(Icons.done_all), onPressed: state.hasLoaded && state.documents.isNotEmpty 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 b583256..37ef002 100644 --- a/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart +++ b/lib/features/labels/tags/view/widgets/fullscreen_tags_form.dart @@ -72,6 +72,7 @@ class _FullscreenTagsFormState extends State { return Scaffold( floatingActionButton: widget.allowCreation ? FloatingActionButton( + heroTag: "fab_tags_form", onPressed: _onAddTag, child: const Icon(Icons.add), ) diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index 5aa210b..ff7b39c 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -66,15 +66,19 @@ class _LabelsPageState extends State child: Scaffold( drawer: const AppDrawer(), floatingActionButton: FloatingActionButton( + heroTag: "fab_labels_page", onPressed: [ if (user.canViewCorrespondents) - () => CreateLabelRoute().push(context), + () => CreateLabelRoute(LabelType.correspondent) + .push(context), if (user.canViewDocumentTypes) - () => CreateLabelRoute().push(context), + () => CreateLabelRoute(LabelType.documentType) + .push(context), if (user.canViewTags) - () => CreateLabelRoute().push(context), + () => CreateLabelRoute(LabelType.tag).push(context), if (user.canViewStoragePaths) - () => CreateLabelRoute().push(context), + () => CreateLabelRoute(LabelType.storagePath) + .push(context), ][_currentIndex], child: const Icon(Icons.add), ), @@ -247,7 +251,8 @@ class _LabelsPageState extends State }, emptyStateActionButtonLabel: S.of(context)!.addNewCorrespondent, emptyStateDescription: S.of(context)!.noCorrespondentsSetUp, - onAddNew: () => CreateLabelRoute().push(context), + onAddNew: () => + CreateLabelRoute(LabelType.correspondent).push(context), ), ], ); @@ -274,7 +279,8 @@ class _LabelsPageState extends State }, emptyStateActionButtonLabel: S.of(context)!.addNewDocumentType, emptyStateDescription: S.of(context)!.noDocumentTypesSetUp, - onAddNew: () => CreateLabelRoute().push(context), + onAddNew: () => + CreateLabelRoute(LabelType.documentType).push(context), ), ], ); @@ -310,7 +316,7 @@ class _LabelsPageState extends State ), emptyStateActionButtonLabel: S.of(context)!.addNewTag, emptyStateDescription: S.of(context)!.noTagsSetUp, - onAddNew: () => CreateLabelRoute().push(context), + onAddNew: () => CreateLabelRoute(LabelType.tag).push(context), ), ], ); @@ -338,7 +344,8 @@ class _LabelsPageState extends State contentBuilder: (path) => Text(path.path), emptyStateActionButtonLabel: S.of(context)!.addNewStoragePath, emptyStateDescription: S.of(context)!.noStoragePathsSetUp, - onAddNew: () => CreateLabelRoute().push(context), + onAddNew: () => + CreateLabelRoute(LabelType.storagePath).push(context), ), ], ); diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index ed10064..e3a047f 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -4,6 +4,7 @@ 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'; import 'package:paperless_mobile/helpers/format_helpers.dart'; +import 'package:paperless_mobile/routes/typed/branches/labels_route.dart'; class LabelItem extends StatelessWidget { final T label; @@ -44,7 +45,7 @@ class LabelItem extends StatelessWidget { onPressed: canOpen ? () { final filter = filterBuilder(label); - pushLinkedDocumentsView(context, filter: filter); + LinkedDocumentsRoute(filter).push(context); } : null, ); diff --git a/lib/features/labels/view/widgets/label_text.dart b/lib/features/labels/view/widgets/label_text.dart index bf15b42..a1b3e1f 100644 --- a/lib/features/labels/view/widgets/label_text.dart +++ b/lib/features/labels/view/widgets/label_text.dart @@ -8,7 +8,7 @@ class LabelText extends StatelessWidget { const LabelText({ super.key, this.style, - this.placeholder = "", + this.placeholder = "-", required this.label, }); diff --git a/lib/features/landing/view/landing_page.dart b/lib/features/landing/view/landing_page.dart index 18a2a9d..ecca02b 100644 --- a/lib/features/landing/view/landing_page.dart +++ b/lib/features/landing/view/landing_page.dart @@ -1,11 +1,7 @@ -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}); diff --git a/lib/features/saved_view/view/add_saved_view_page.dart b/lib/features/saved_view/view/add_saved_view_page.dart index e0b6e88..e24ad87 100644 --- a/lib/features/saved_view/view/add_saved_view_page.dart +++ b/lib/features/saved_view/view/add_saved_view_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:go_router/go_router.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -39,6 +40,7 @@ class _AddSavedViewPageState extends State { title: Text(S.of(context)!.newView), ), floatingActionButton: FloatingActionButton.extended( + heroTag: "fab_add_saved_view_page", icon: const Icon(Icons.add), onPressed: () => _onCreate(context), label: Text(S.of(context)!.create), @@ -102,8 +104,7 @@ class _AddSavedViewPageState extends State { void _onCreate(BuildContext context) { if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) { - Navigator.pop( - context, + context.pop( SavedView.fromDocumentFilter( DocumentFilterForm.assembleFilter( _filterFormKey, 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 352e7a2..a98634b 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 @@ -47,7 +47,7 @@ class _SavedViewDetailsPageState extends State false; if (shouldDelete) { await widget.onDelete(cubit.savedView); - Navigator.pop(context); + context.pop(context); } }, ), diff --git a/lib/main.dart b/lib/main.dart index 2eebcf1..5c14e10 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,6 +37,7 @@ import 'package:paperless_mobile/features/notifications/services/local_notificat 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/routes/navigation_keys.dart'; +import 'package:paperless_mobile/routes/routes.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'; @@ -163,9 +164,7 @@ void main() async { BlocProvider.value(value: connectivityCubit), BlocProvider.value(value: authenticationCubit), ], - child: GoRouterShell( - apiFactory: apiFactory, - ), + child: GoRouterShell(apiFactory: apiFactory), ), ), ); @@ -178,65 +177,9 @@ void main() async { debugPrint("An unepxected exception has occured!"); debugPrint(message); debugPrintStack(stackTrace: stack); - // if (_rootScaffoldKey.currentContext != null) { - // ScaffoldMessenger.maybeOf(_rootScaffoldKey.currentContext!) - // ?..hideCurrentSnackBar() - // ..showSnackBar(SnackBar(content: Text(message))); - // } }); } -// 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({ @@ -252,21 +195,19 @@ class _GoRouterShellState 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 + FlutterNativeSplash.remove(); if (Platform.isAndroid) { _setOptimalDisplayMode(); } initializeDateFormatting(); } + /// Activates the highest supported refresh rate on the device. Future _setOptimalDisplayMode() async { final List supported = await FlutterDisplayMode.supported; final DisplayMode active = await FlutterDisplayMode.active; @@ -284,7 +225,7 @@ class _GoRouterShellState extends State { } late final _router = GoRouter( - debugLogDiagnostics: true, + debugLogDiagnostics: kDebugMode, initialLocation: "/login", routes: [ $loginRoute, @@ -348,23 +289,20 @@ class _GoRouterShellState extends State { @override Widget build(BuildContext context) { - 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( + return BlocListener( + listener: (context, state) { + state.when( + unauthenticated: () => _router.goNamed(R.login), + requriresLocalAuthentication: () => _router.goNamed(R.verifyIdentity), + switchingAccounts: () => _router.goNamed(R.switchingAccounts), + authenticated: (localUserId) => _router.goNamed(R.landing), + ); + }, + child: GlobalSettingsBuilder( + builder: (context, settings) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + return MaterialApp.router( routerConfig: _router, debugShowCheckedModeBanner: true, title: "Paperless Mobile", @@ -384,79 +322,11 @@ class _GoRouterShellState extends State { languageCode: settings.preferredLocaleSubtag, ), localizationsDelegates: S.localizationsDelegates, - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ); } } - -// 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/routes.dart b/lib/routes/routes.dart index 291ff43..308003a 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -17,4 +17,6 @@ class R { static const inbox = "inbox"; static const documentPreview = "documentPreview"; static const settings = "settings"; + static const linkedDocuments = "linkedDocuments"; + static const bulkEditDocuments = "bulkEditDocuments"; } diff --git a/lib/routes/typed/branches/documents_route.dart b/lib/routes/typed/branches/documents_route.dart index 6174838..556f942 100644 --- a/lib/routes/typed/branches/documents_route.dart +++ b/lib/routes/typed/branches/documents_route.dart @@ -1,17 +1,17 @@ -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_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.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/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/routes.dart'; @@ -37,7 +37,11 @@ class DocumentsBranch extends StatefulShellBranchData { TypedGoRoute( path: "preview", name: R.documentPreview, - ) + ), + TypedGoRoute( + path: "bulk-edit", + name: R.bulkEditDocuments, + ), ], ) class DocumentsRoute extends GoRouteData { @@ -101,13 +105,115 @@ class EditDocumentRoute extends GoRouteData { } class DocumentPreviewRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + final DocumentModel $extra; - const DocumentPreviewRoute(this.$extra); + final String? title; + + const DocumentPreviewRoute({ + required this.$extra, + this.title, + }); @override Widget build(BuildContext context, GoRouterState state) { return DocumentView( documentBytes: context.read().download($extra), + title: title ?? $extra.title, + ); + } +} + +class BulkEditExtraWrapper { + final List selection; + final LabelType type; + + const BulkEditExtraWrapper(this.selection, this.type); +} + +class BulkEditDocumentsRoute extends GoRouteData { + /// Selection + final BulkEditExtraWrapper $extra; + BulkEditDocumentsRoute(this.$extra); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BlocProvider( + create: (_) => DocumentBulkActionCubit( + context.read(), + context.read(), + context.read(), + selection: $extra.selection, + ), + child: BlocBuilder( + builder: (context, state) { + return switch ($extra.type) { + LabelType.tag => const FullscreenBulkEditTagsWidget(), + _ => FullscreenBulkEditLabelPage( + options: switch ($extra.type) { + LabelType.correspondent => state.correspondents, + LabelType.documentType => state.documentTypes, + LabelType.storagePath => state.storagePaths, + _ => throw Exception("Parameter not allowed here."), + }, + selection: state.selection, + labelMapper: (document) { + return switch ($extra.type) { + LabelType.correspondent => document.correspondent, + LabelType.documentType => document.documentType, + LabelType.storagePath => document.storagePath, + _ => throw Exception("Parameter not allowed here."), + }; + }, + leadingIcon: switch ($extra.type) { + LabelType.correspondent => const Icon(Icons.person_outline), + LabelType.documentType => + const Icon(Icons.description_outlined), + LabelType.storagePath => const Icon(Icons.folder_outlined), + _ => throw Exception("Parameter not allowed here."), + }, + hintText: S.of(context)!.startTyping, + onSubmit: switch ($extra.type) { + LabelType.correspondent => context + .read() + .bulkModifyCorrespondent, + LabelType.documentType => context + .read() + .bulkModifyDocumentType, + LabelType.storagePath => context + .read() + .bulkModifyStoragePath, + _ => throw Exception("Parameter not allowed here."), + }, + assignMessageBuilder: (int count, String name) { + return switch ($extra.type) { + LabelType.correspondent => S + .of(context)! + .bulkEditCorrespondentAssignMessage(name, count), + LabelType.documentType => S + .of(context)! + .bulkEditDocumentTypeAssignMessage(count, name), + LabelType.storagePath => S + .of(context)! + .bulkEditDocumentTypeAssignMessage(count, name), + _ => throw Exception("Parameter not allowed here."), + }; + }, + removeMessageBuilder: (int count) { + return switch ($extra.type) { + LabelType.correspondent => + S.of(context)!.bulkEditCorrespondentRemoveMessage(count), + LabelType.documentType => + S.of(context)!.bulkEditDocumentTypeRemoveMessage(count), + LabelType.storagePath => + S.of(context)!.bulkEditStoragePathRemoveMessage(count), + _ => throw Exception("Parameter not allowed here."), + }; + }, + ), + }; + }, + ), ); } } diff --git a/lib/routes/typed/branches/labels_route.dart b/lib/routes/typed/branches/labels_route.dart index e1deac8..c70fc5d 100644 --- a/lib/routes/typed/branches/labels_route.dart +++ b/lib/routes/typed/branches/labels_route.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.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/edit_label/view/impl/add_correspondent_page.dart'; @@ -10,6 +11,8 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_typ 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/features/linked_documents/cubit/linked_documents_cubit.dart'; +import 'package:paperless_mobile/features/linked_documents/view/linked_documents_page.dart'; import 'package:paperless_mobile/routes/navigation_keys.dart'; import 'package:paperless_mobile/routes/routes.dart'; @@ -32,6 +35,10 @@ class LabelsBranch extends StatefulShellBranchData { path: "create", name: R.createLabel, ), + TypedGoRoute( + path: "linked-documents", + name: R.linkedDocuments, + ), ], ) class LabelsRoute extends GoRouteData { @@ -59,26 +66,42 @@ class EditLabelRoute extends GoRouteData { } } -class CreateLabelRoute extends GoRouteData { +class CreateLabelRoute extends GoRouteData { static final GlobalKey $parentNavigatorKey = rootNavigatorKey; - + final LabelType $extra; final String? name; - CreateLabelRoute({ + CreateLabelRoute( + this.$extra, { 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(); + return switch ($extra) { + LabelType.correspondent => AddCorrespondentPage(initialName: name), + LabelType.documentType => AddDocumentTypePage(initialName: name), + LabelType.tag => AddTagPage(initialName: name), + LabelType.storagePath => AddStoragePathPage(initialName: name), + }; + } +} + +class LinkedDocumentsRoute extends GoRouteData { + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + final DocumentFilter $extra; + + const LinkedDocumentsRoute(this.$extra); + @override + Widget build(BuildContext context, GoRouterState state) { + return BlocProvider( + create: (context) => LinkedDocumentsCubit( + $extra, + context.read(), + context.read(), + context.read(), + ), + child: const LinkedDocumentsPage(), + ); } } 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 5970696..ab4c49e 100644 --- a/packages/paperless_api/lib/src/models/labels/label_model.dart +++ b/packages/paperless_api/lib/src/models/labels/label_model.dart @@ -8,6 +8,13 @@ import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; part 'label_model.g.dart'; +enum LabelType { + correspondent, + documentType, + tag, + storagePath, +} + sealed class Label extends Equatable implements Comparable { static const idKey = "id"; static const nameKey = "name"; diff --git a/packages/paperless_document_scanner/example/lib/scan.dart b/packages/paperless_document_scanner/example/lib/scan.dart index aed8a26..a3bf9a1 100644 --- a/packages/paperless_document_scanner/example/lib/scan.dart +++ b/packages/paperless_document_scanner/example/lib/scan.dart @@ -124,19 +124,22 @@ class _ScanState extends State { imagePath = filePath; }); - EdgeDetectionResult result = await EdgeDetector().detectEdgesFromFile(filePath); + EdgeDetectionResult result = + await EdgeDetector().detectEdgesFromFile(filePath); setState(() { edgeDetectionResult = result; }); } - Future _processImage(String filePath, EdgeDetectionResult edgeDetectionResult) async { + Future _processImage( + String filePath, EdgeDetectionResult edgeDetectionResult) async { if (!mounted) { return; } - bool result = await EdgeDetector().processImageFromFile(filePath, edgeDetectionResult); + bool result = await EdgeDetector() + .processImageFromFile(filePath, edgeDetectionResult); if (result == false) { return;