From 2f31d9c05314cf70977edd9a3b3fbe96287300ae Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Mon, 12 Dec 2022 01:29:34 +0100 Subject: [PATCH] WIP - more decoupling of blocs --- .../authentication.interceptor.dart | 6 + .../interceptor/base_url_interceptor.dart | 6 + .../language_header.interceptor.dart | 6 + .../response_conversion.interceptor.dart | 9 +- .../impl/correspondent_repository_impl.dart | 3 + .../impl/document_type_repository_impl.dart | 3 + .../impl/storage_path_repository_impl.dart | 3 + .../repository/impl/tag_repository_impl.dart | 3 + lib/core/repository/label_repository.dart | 2 + lib/core/widgets/error_report_page.dart | 3 +- lib/di_modules.dart | 3 +- lib/extensions/flutter_extensions.dart | 27 +- .../bloc/document_details_cubit.dart | 14 +- .../bloc/document_details_state.dart | 4 +- .../view/pages/document_details_page.dart | 108 +++---- .../widgets/document_download_button.dart | 3 +- .../document_upload_preparation_page.dart | 3 +- .../view/pages/document_edit_page.dart | 264 ++++++++---------- .../documents/view/pages/documents_page.dart | 241 ++++++++-------- .../view/widgets/documents_empty_state.dart | 7 +- .../widgets/search/document_filter_panel.dart | 162 +++++------ .../sort_field_selection_bottom_sheet.dart | 61 +++- .../selection/documents_page_app_bar.dart | 25 +- .../view/widgets/sort_documents_button.dart | 64 +++-- .../cubit/edit_document_cubit.dart | 91 ++++++ .../cubit/edit_document_state.dart | 43 +++ .../edit_label/cubit/edit_label_cubit.dart | 4 +- .../edit_label/view/add_label_page.dart | 2 +- .../edit_label/view/edit_label_page.dart | 53 ++-- .../view/impl/edit_document_type_page.dart | 4 +- .../view/impl/edit_storage_path_page.dart | 4 +- .../edit_label/view/impl/edit_tag_page.dart | 4 +- lib/features/edit_label/view/label_form.dart | 7 +- lib/features/home/view/home_page.dart | 5 + .../home/view/widget/info_drawer.dart | 6 +- lib/features/inbox/view/pages/inbox_page.dart | 15 +- lib/features/labels/bloc/label_cubit.dart | 12 +- .../tags/view/widgets/tags_form_field.dart | 245 ++++++++-------- .../labels/view/pages/labels_page.dart | 1 + .../labels/view/widgets/label_form_field.dart | 72 +++-- .../labels/view/widgets/label_tab_view.dart | 27 +- .../login/bloc/authentication_cubit.dart | 10 +- .../view/saved_view_selection_widget.dart | 25 +- lib/features/scan/view/scanner_page.dart | 1 + lib/main.dart | 11 +- .../lib/src/models/document_filter.dart | 23 +- .../lib/src/models/document_model.dart | 12 +- .../paperless_documents_api_impl.dart | 2 +- pubspec.lock | 171 ++++++------ pubspec.yaml | 2 +- test/src/bloc/document_cubit_test.dart | 1 - 51 files changed, 1083 insertions(+), 800 deletions(-) create mode 100644 lib/features/edit_document/cubit/edit_document_cubit.dart create mode 100644 lib/features/edit_document/cubit/edit_document_state.dart diff --git a/lib/core/interceptor/authentication.interceptor.dart b/lib/core/interceptor/authentication.interceptor.dart index 43e2252..9c0a29d 100644 --- a/lib/core/interceptor/authentication.interceptor.dart +++ b/lib/core/interceptor/authentication.interceptor.dart @@ -34,4 +34,10 @@ class AuthenticationInterceptor implements InterceptorContract { Future interceptResponse( {required BaseResponse response}) async => response; + + @override + Future shouldInterceptRequest() async => true; + + @override + Future shouldInterceptResponse() async => true; } diff --git a/lib/core/interceptor/base_url_interceptor.dart b/lib/core/interceptor/base_url_interceptor.dart index 43189ce..68424de 100644 --- a/lib/core/interceptor/base_url_interceptor.dart +++ b/lib/core/interceptor/base_url_interceptor.dart @@ -25,4 +25,10 @@ class BaseUrlInterceptor implements InterceptorContract { Future interceptResponse( {required BaseResponse response}) async => response; + + @override + Future shouldInterceptRequest() async => true; + + @override + Future shouldInterceptResponse() async => true; } diff --git a/lib/core/interceptor/language_header.interceptor.dart b/lib/core/interceptor/language_header.interceptor.dart index 247d4db..f50e704 100644 --- a/lib/core/interceptor/language_header.interceptor.dart +++ b/lib/core/interceptor/language_header.interceptor.dart @@ -25,4 +25,10 @@ class LanguageHeaderInterceptor implements InterceptorContract { Future interceptResponse( {required BaseResponse response}) async => response; + + @override + Future shouldInterceptRequest() async => true; + + @override + Future shouldInterceptResponse() async => true; } diff --git a/lib/core/interceptor/response_conversion.interceptor.dart b/lib/core/interceptor/response_conversion.interceptor.dart index 69c3729..a25e3c6 100644 --- a/lib/core/interceptor/response_conversion.interceptor.dart +++ b/lib/core/interceptor/response_conversion.interceptor.dart @@ -1,5 +1,4 @@ -import 'package:http/http.dart'; -import 'package:http_interceptor/http/http.dart'; +import 'package:http_interceptor/http_interceptor.dart'; import 'package:injectable/injectable.dart'; const interceptedRoutes = ['thumb/']; @@ -33,4 +32,10 @@ class ResponseConversionInterceptor implements InterceptorContract { } return response; } + + @override + Future shouldInterceptRequest() async => true; + + @override + Future shouldInterceptResponse() async => true; } diff --git a/lib/core/repository/impl/correspondent_repository_impl.dart b/lib/core/repository/impl/correspondent_repository_impl.dart index cf2ae74..52af5ec 100644 --- a/lib/core/repository/impl/correspondent_repository_impl.dart +++ b/lib/core/repository/impl/correspondent_repository_impl.dart @@ -64,4 +64,7 @@ class CorrespondentRepositoryImpl implements LabelRepository { void clear() { _subject.add(const {}); } + + @override + Map get current => _subject.value; } diff --git a/lib/core/repository/impl/document_type_repository_impl.dart b/lib/core/repository/impl/document_type_repository_impl.dart index 60ee381..2dbb079 100644 --- a/lib/core/repository/impl/document_type_repository_impl.dart +++ b/lib/core/repository/impl/document_type_repository_impl.dart @@ -63,4 +63,7 @@ class DocumentTypeRepositoryImpl implements LabelRepository { void clear() { _subject.add(const {}); } + + @override + Map get current => _subject.value; } diff --git a/lib/core/repository/impl/storage_path_repository_impl.dart b/lib/core/repository/impl/storage_path_repository_impl.dart index 2e3d608..c555fa9 100644 --- a/lib/core/repository/impl/storage_path_repository_impl.dart +++ b/lib/core/repository/impl/storage_path_repository_impl.dart @@ -63,4 +63,7 @@ class StoragePathRepositoryImpl implements LabelRepository { void clear() { _subject.add(const {}); } + + @override + Map get current => _subject.value; } diff --git a/lib/core/repository/impl/tag_repository_impl.dart b/lib/core/repository/impl/tag_repository_impl.dart index c479cea..437512d 100644 --- a/lib/core/repository/impl/tag_repository_impl.dart +++ b/lib/core/repository/impl/tag_repository_impl.dart @@ -62,4 +62,7 @@ class TagRepositoryImpl implements LabelRepository { void clear() { _subject.add(const {}); } + + @override + Map get current => _subject.value; } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index 848681c..20b38c2 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -3,6 +3,8 @@ import 'package:paperless_api/paperless_api.dart'; abstract class LabelRepository { Stream> get labels; + Map get current; + Future create(T label); Future find(int id); Future> findAll([Iterable? ids]); diff --git a/lib/core/widgets/error_report_page.dart b/lib/core/widgets/error_report_page.dart index e93eb38..b6bfe17 100644 --- a/lib/core/widgets/error_report_page.dart +++ b/lib/core/widgets/error_report_page.dart @@ -71,8 +71,7 @@ Note: If you have the GitHub Android app installed, the descriptions will not be Text( 'Stack Trace', style: Theme.of(context).textTheme.subtitle1, - ).padded( - const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0)), + ).paddedOnly(top: 8.0, left: 8.0, right: 8.0), TextButton.icon( label: const Text('Copy'), icon: const Icon(Icons.copy), diff --git a/lib/di_modules.dart b/lib/di_modules.dart index b99961d..b4e5b5e 100644 --- a/lib/di_modules.dart +++ b/lib/di_modules.dart @@ -3,13 +3,12 @@ import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:http_interceptor/http_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/base_url_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart'; -import 'package:http/http.dart'; import 'package:http/io_client.dart'; -import 'package:http_interceptor/http/http.dart'; import 'package:injectable/injectable.dart'; import 'package:local_auth/local_auth.dart'; diff --git a/lib/extensions/flutter_extensions.dart b/lib/extensions/flutter_extensions.dart index 51fb94d..d599d11 100644 --- a/lib/extensions/flutter_extensions.dart +++ b/lib/extensions/flutter_extensions.dart @@ -1,9 +1,32 @@ import 'package:flutter/widgets.dart'; extension WidgetPadding on Widget { - Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(8)]) { + Widget padded([double all = 8.0]) { return Padding( - padding: value, + padding: EdgeInsets.all(all), + child: this, + ); + } + + Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), + child: this, + ); + } + + Widget paddedOnly( + {double top = 0.0, + double bottom = 0.0, + double left = 0.0, + double right = 0.0}) { + return Padding( + padding: EdgeInsets.only( + top: top, + bottom: bottom, + left: left, + right: right, + ), child: this, ); } diff --git a/lib/features/document_details/bloc/document_details_cubit.dart b/lib/features/document_details/bloc/document_details_cubit.dart index 68f437f..028893a 100644 --- a/lib/features/document_details/bloc/document_details_cubit.dart +++ b/lib/features/document_details/bloc/document_details_cubit.dart @@ -12,18 +12,18 @@ class DocumentDetailsCubit extends Cubit { Future delete(DocumentModel document) async { await _api.delete(document); - emit(const DocumentDetailsState()); - } - - Future update(DocumentModel document) async { - final updatedDocument = await _api.update(document); - emit(DocumentDetailsState(document: updatedDocument)); } Future assignAsn(DocumentModel document) async { if (document.archiveSerialNumber == null) { final int asn = await _api.findNextAsn(); - update(document.copyWith(archiveSerialNumber: asn)); + final updatedDocument = + await _api.update(document.copyWith(archiveSerialNumber: asn)); + emit(DocumentDetailsState(document: updatedDocument)); } } + + void replaceDocument(DocumentModel document) { + emit(DocumentDetailsState(document: document)); + } } diff --git a/lib/features/document_details/bloc/document_details_state.dart b/lib/features/document_details/bloc/document_details_state.dart index 8bbca82..81d2bfd 100644 --- a/lib/features/document_details/bloc/document_details_state.dart +++ b/lib/features/document_details/bloc/document_details_state.dart @@ -1,10 +1,10 @@ part of 'document_details_cubit.dart'; class DocumentDetailsState with EquatableMixin { - final DocumentModel? document; + final DocumentModel document; const DocumentDetailsState({ - this.document, + required this.document, }); @override 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 1e74c3b..be5f951 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -18,6 +18,7 @@ import 'package:paperless_mobile/features/documents/view/pages/document_edit_pag 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'; +import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; @@ -65,9 +66,13 @@ class _DocumentDetailsPageState extends State { child: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButton: widget.allowEdit - ? FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: _onEdit, + ? BlocBuilder( + builder: (context, state) { + return FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: () => _onEdit(state.document), + ); + }, ) : null, bottomNavigationBar: @@ -79,24 +84,20 @@ class _DocumentDetailsPageState extends State { children: [ IconButton( icon: const Icon(Icons.delete), - onPressed: widget.allowEdit && state.document != null - ? () => _onDelete(state.document!) + onPressed: widget.allowEdit + ? () => _onDelete(state.document) : null, - ).padded(const EdgeInsets.symmetric(horizontal: 4)), + ).paddedSymmetrically(horizontal: 4), DocumentDownloadButton( document: state.document, ), IconButton( icon: const Icon(Icons.open_in_new), - onPressed: state.document != null - ? () => _onOpen(state.document!) - : null, - ).padded(const EdgeInsets.only(right: 4)), + onPressed: () => _onOpen(state.document), + ).paddedOnly(right: 4.0), IconButton( icon: const Icon(Icons.share), - onPressed: state.document != null - ? () => _onShare(state.document!) - : null, + onPressed: () => _onShare(state.document), ), ], ), @@ -123,15 +124,10 @@ class _DocumentDetailsPageState extends State { expandedHeight: 200.0, flexibleSpace: BlocBuilder( - builder: (context, state) { - if (state.document == null) { - return Container(height: 200); - } - return DocumentPreview( - id: state.document!.id, - fit: BoxFit.cover, - ); - }, + builder: (context, state) => DocumentPreview( + id: state.document.id, + fit: BoxFit.cover, + ), ), bottom: ColoredTabBar( backgroundColor: @@ -172,27 +168,18 @@ class _DocumentDetailsPageState extends State { ], body: BlocBuilder( builder: (context, state) { - if (state.document == null) { - return TabBarView( - children: [ - Container(), - Container(), - Container(), - ], - ); - } return TabBarView( children: [ _buildDocumentOverview( - state.document!, + state.document, widget.titleAndContentQueryString, ), _buildDocumentContentView( - state.document!, + state.document, widget.titleAndContentQueryString, ), _buildDocumentMetaDataView( - state.document!, + state.document, ), ].padded(), ); @@ -204,47 +191,42 @@ class _DocumentDetailsPageState extends State { ); } - Future _onEdit() async { + Future _onEdit(DocumentModel document) async { { final cubit = BlocProvider.of(context); - if (cubit.state.document == null) { - return; - } Navigator.push( context, MaterialPageRoute( - builder: (_) => MultiRepositoryProvider( - providers: [ - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + builder: (context) => BlocProvider( + create: (context) => EditDocumentCubit( + document, + documentsApi: getIt(), + correspondentRepository: + RepositoryProvider.of>( + context, ), - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + documentTypeRepository: + RepositoryProvider.of>( + context, ), - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + storagePathRepository: + RepositoryProvider.of>( + context, ), - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), + tagRepository: RepositoryProvider.of>( + context, ), - ], - child: DocumentEditPage( - document: cubit.state.document!, - onEdit: (updatedDocument) { - return BlocProvider.of(context) - .update(updatedDocument); + ), + child: BlocListener( + listenWhen: (previous, current) => + previous.document != current.document, + listener: (context, state) { + cubit.replaceDocument(state.document); }, + child: const DocumentEditPage(), ), ), - maintainState: false, + maintainState: true, ), ); } diff --git a/lib/features/document_details/view/widgets/document_download_button.dart b/lib/features/document_details/view/widgets/document_download_button.dart index 3de5f73..614d825 100644 --- a/lib/features/document_details/view/widgets/document_download_button.dart +++ b/lib/features/document_details/view/widgets/document_download_button.dart @@ -1,4 +1,3 @@ -import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -33,7 +32,7 @@ class _DocumentDownloadButtonState extends State { onPressed: Platform.isAndroid && widget.document != null ? () => _onDownload(widget.document!) : null, - ).padded(const EdgeInsets.only(right: 4)); + ).paddedOnly(right: 4); } Future _onDownload(DocumentModel document) async { 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 80bc127..7050060 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -200,11 +200,12 @@ class _DocumentUploadPreparationPageState CorrespondentQuery.notAssigned, prefixIcon: const Icon(Icons.person_outline), ), - const TagFormField( + TagFormField( name: DocumentModel.tagsKey, notAssignedSelectable: false, anyAssignedSelectable: false, excludeAllowed: false, + selectableOptions: state.tags, //Label: "Tags" + " *", ), Text( diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 2e13b61..6389ed6 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,27 +7,19 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.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/labels/bloc/label_cubit.dart'; -import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; class DocumentEditPage extends StatefulWidget { - final DocumentModel document; - final FutureOr Function(DocumentModel updatedDocument) onEdit; - const DocumentEditPage({ Key? key, - required this.document, - required this.onEdit, }) : super(key: key); @override @@ -43,150 +34,133 @@ class _DocumentEditPageState extends State { static const fkCreatedDate = "createdAtDate"; static const fkStoragePath = 'storagePath'; - late Future documentBytes; - final GlobalKey _formKey = GlobalKey(); bool _isSubmitLoading = false; @override - void initState() { - super.initState(); - documentBytes = - getIt().getPreview(widget.document.id); + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Scaffold( + resizeToAvoidBottomInset: false, + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _onSubmit(state.document), + icon: const Icon(Icons.save), + label: Text(S.of(context).genericActionSaveLabel), + ), + appBar: AppBar( + title: Text(S.of(context).documentEditPageTitle), + bottom: _isSubmitLoading + ? const PreferredSize( + preferredSize: Size.fromHeight(4), + child: LinearProgressIndicator(), + ) + : null, + ), + extendBody: true, + body: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + top: 8, + left: 8, + right: 8, + ), + child: FormBuilder( + key: _formKey, + child: ListView(children: [ + _buildTitleFormField(state.document.title).padded(), + _buildCreatedAtFormField(state.document.created).padded(), + _buildDocumentTypeFormField( + state.document.documentType, state.documentTypes) + .padded(), + _buildCorrespondentFormField( + state.document.correspondent, state.correspondents) + .padded(), + _buildStoragePathFormField( + state.document.storagePath, state.storagePaths) + .padded(), + TagFormField( + initialValue: IdsTagsQuery.included(state.document.tags), + notAssignedSelectable: false, + anyAssignedSelectable: false, + excludeAllowed: false, + name: fkTags, + selectableOptions: state.tags, + ).padded(), + ]), + ), + )); + }, + ); } - @override - Widget build(BuildContext context) { - return LabelsBlocProvider( - child: Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - onPressed: _onSubmit, - icon: const Icon(Icons.save), - label: Text(S.of(context).genericActionSaveLabel), + Widget _buildStoragePathFormField( + int? initialId, Map options) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( + value: RepositoryProvider.of>(context), + child: AddStoragePathPage(initalValue: initialValue), + ), + label: S.of(context).documentStoragePathPropertyLabel, + state: options, + initialValue: StoragePathQuery.fromId(initialId), + name: fkStoragePath, + queryParameterIdBuilder: StoragePathQuery.fromId, + queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, + prefixIcon: const Icon(Icons.folder_outlined), + ); + } + + Widget _buildCorrespondentFormField( + int? initialId, Map options) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value( + value: RepositoryProvider.of>( + context, ), - appBar: AppBar( - title: Text(S.of(context).documentEditPageTitle), - bottom: _isSubmitLoading - ? const PreferredSize( - preferredSize: Size.fromHeight(4), - child: LinearProgressIndicator(), - ) - : null, + child: AddCorrespondentPage(initialName: initialValue), + ), + label: S.of(context).documentCorrespondentPropertyLabel, + state: options, + initialValue: CorrespondentQuery.fromId(initialId), + name: fkCorrespondent, + queryParameterIdBuilder: CorrespondentQuery.fromId, + queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, + prefixIcon: const Icon(Icons.person_outlined), + ); + } + + Widget _buildDocumentTypeFormField( + int? initialId, Map options) { + return LabelFormField( + notAssignedSelectable: false, + formBuilderState: _formKey.currentState, + labelCreationWidgetBuilder: (currentInput) => RepositoryProvider.value( + value: RepositoryProvider.of>( + context, ), - extendBody: true, - body: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - top: 8, - left: 8, - right: 8, - ), - child: FormBuilder( - key: _formKey, - child: ListView(children: [ - _buildTitleFormField().padded(), - _buildCreatedAtFormField().padded(), - _buildDocumentTypeFormField().padded(), - _buildCorrespondentFormField().padded(), - _buildStoragePathFormField().padded(), - TagFormField( - initialValue: IdsTagsQuery.included(widget.document.tags), - notAssignedSelectable: false, - anyAssignedSelectable: false, - excludeAllowed: false, - name: fkTags, - ).padded(), - ]), - ), + child: AddDocumentTypePage( + initialName: currentInput, ), ), + label: S.of(context).documentDocumentTypePropertyLabel, + initialValue: DocumentTypeQuery.fromId(initialId), + state: options, + name: fkDocumentType, + queryParameterIdBuilder: DocumentTypeQuery.fromId, + queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, + prefixIcon: const Icon(Icons.description_outlined), ); } - BlocBuilder, LabelState> - _buildStoragePathFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => - RepositoryProvider.value( - value: RepositoryProvider.of>(context), - child: AddStoragePathPage(initalValue: initialValue), - ), - label: S.of(context).documentStoragePathPropertyLabel, - state: state.labels, - initialValue: StoragePathQuery.fromId(widget.document.storagePath), - name: fkStoragePath, - queryParameterIdBuilder: StoragePathQuery.fromId, - queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, - prefixIcon: const Icon(Icons.folder_outlined), - ); - }, - ); - } - - BlocBuilder, LabelState> - _buildCorrespondentFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (initialValue) => - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), - child: AddCorrespondentPage(initialName: initialValue), - ), - label: S.of(context).documentCorrespondentPropertyLabel, - state: state.labels, - initialValue: - CorrespondentQuery.fromId(widget.document.correspondent), - name: fkCorrespondent, - queryParameterIdBuilder: CorrespondentQuery.fromId, - queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, - prefixIcon: const Icon(Icons.person_outlined), - ); - }, - ); - } - - BlocBuilder, LabelState> - _buildDocumentTypeFormField() { - return BlocBuilder, LabelState>( - builder: (context, state) { - return LabelFormField( - notAssignedSelectable: false, - formBuilderState: _formKey.currentState, - labelCreationWidgetBuilder: (currentInput) => - RepositoryProvider.value( - value: RepositoryProvider.of>( - context, - ), - child: AddDocumentTypePage( - initialName: currentInput, - ), - ), - label: S.of(context).documentDocumentTypePropertyLabel, - initialValue: DocumentTypeQuery.fromId(widget.document.documentType), - state: state.labels, - name: fkDocumentType, - queryParameterIdBuilder: DocumentTypeQuery.fromId, - queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, - prefixIcon: const Icon(Icons.description_outlined), - ); - }, - ); - } - - Future _onSubmit() async { + Future _onSubmit(DocumentModel document) async { if (_formKey.currentState?.saveAndValidate() ?? false) { final values = _formKey.currentState!.value; - var updatedDocument = widget.document.copyWith( + var mergedDocument = document.copyWith( title: values[fkTitle], created: values[fkCreatedDate], overwriteDocumentType: true, @@ -201,9 +175,9 @@ class _DocumentEditPageState extends State { setState(() { _isSubmitLoading = true; }); - try { - await widget.onEdit(updatedDocument); + await BlocProvider.of(context) + .updateDocument(mergedDocument); showSnackBar(context, S.of(context).documentUpdateSuccessMessage); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); @@ -216,18 +190,18 @@ class _DocumentEditPageState extends State { } } - Widget _buildTitleFormField() { + Widget _buildTitleFormField(String? initialTitle) { return FormBuilderTextField( name: fkTitle, validator: FormBuilderValidators.required(), decoration: InputDecoration( label: Text(S.of(context).documentTitlePropertyLabel), ), - initialValue: widget.document.title, + initialValue: initialTitle, ); } - Widget _buildCreatedAtFormField() { + Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) { return FormBuilderDateTimePicker( inputType: InputType.date, name: fkCreatedDate, @@ -235,7 +209,7 @@ class _DocumentEditPageState extends State { prefixIcon: const Icon(Icons.calendar_month_outlined), label: Text(S.of(context).documentCreatedPropertyLabel), ), - initialValue: widget.document.created, + initialValue: initialCreatedAtDate, format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format initialEntryMode: DatePickerEntryMode.calendar, ); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 1ab43d6..7890922 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,10 +1,10 @@ +import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; @@ -24,7 +24,6 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:sliding_up_panel/sliding_up_panel.dart'; class DocumentsPage extends StatefulWidget { const DocumentsPage({Key? key}) : super(key: key); @@ -35,16 +34,17 @@ class DocumentsPage extends StatefulWidget { class _DocumentsPageState extends State { late final DocumentsCubit _documentsCubit; + late final SavedViewCubit _savedViewCubit; + final _pagingController = PagingController( firstPageKey: 1, ); - final _filterPanelController = PanelController(); - @override void initState() { super.initState(); _documentsCubit = BlocProvider.of(context); + _savedViewCubit = BlocProvider.of(context); try { _documentsCubit.load(); } on PaperlessServerException catch (error, stackTrace) { @@ -59,97 +59,69 @@ class _DocumentsPageState extends State { super.dispose(); } - Future _loadNewPage(int pageKey) async { - final pageCount = _documentsCubit.state - .inferPageCount(pageSize: _documentsCubit.state.filter.pageSize); - if (pageCount <= pageKey + 1) { - _pagingController.nextPageKey = null; - } - try { - await _documentsCubit.loadMore(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - - void _onSelected(DocumentModel model) { - _documentsCubit.toggleDocumentSelection(model); - } - - Future _onRefresh() async { - try { - await _documentsCubit.updateCurrentFilter( - (filter) => filter.copyWith(page: 1), - ); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } - @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - if (_filterPanelController.isPanelOpen) { - FocusScope.of(context).unfocus(); - _filterPanelController.close(); - return false; - } - if (_documentsCubit.state.selection.isNotEmpty) { - _documentsCubit.resetSelection(); - return false; - } - return true; + return BlocConsumer( + listenWhen: (previous, current) => + previous != ConnectivityState.connected && + current == ConnectivityState.connected, + listener: (context, state) { + _documentsCubit.load(); }, - child: BlocConsumer( - listenWhen: (previous, current) => - previous != ConnectivityState.connected && - current == ConnectivityState.connected, - listener: (context, state) { - _documentsCubit.load(); - }, - builder: (context, connectivityState) { - return Scaffold( + builder: (context, connectivityState) { + return Scaffold( drawer: BlocProvider.value( value: BlocProvider.of(context), child: InfoDrawer( afterInboxClosed: () => _documentsCubit.reload(), ), ), - resizeToAvoidBottomInset: true, - body: SlidingUpPanel( - backdropEnabled: true, - parallaxEnabled: true, - parallaxOffset: .5, - controller: _filterPanelController, - defaultPanelState: PanelState.CLOSED, - minHeight: 48, - maxHeight: (MediaQuery.of(context).size.height * 3) / 4, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - body: _buildBody(connectivityState), - color: Theme.of(context).scaffoldBackgroundColor, - panelBuilder: (scrollController) => - BlocBuilder( - builder: (context, state) { - return LabelsBlocProvider( - child: DocumentFilterPanel( - panelController: _filterPanelController, - scrollController: scrollController, - initialFilter: state.filter, - onFilterChanged: (filter) => - _documentsCubit.updateFilter(filter: filter), - ), - ); - }, - ), + floatingActionButton: BlocBuilder( + builder: (context, state) { + final appliedFiltersCount = state.filter.appliedFiltersCount; + return Badge( + toAnimate: false, + showBadge: appliedFiltersCount > 0, + badgeContent: appliedFiltersCount > 0 + ? Text(state.filter.appliedFiltersCount.toString()) + : null, + child: FloatingActionButton( + child: const Icon(Icons.filter_alt), + onPressed: _openDocumentFilter, + ), + ); + }, ), - ); - }, + resizeToAvoidBottomInset: true, + body: _buildBody(connectivityState)); + }, + ); + } + + void _openDocumentFilter() async { + final filter = await showModalBottomSheet( + context: context, + builder: (context) => SizedBox( + height: MediaQuery.of(context).size.height - kToolbarHeight - 16, + child: LabelsBlocProvider( + child: DocumentFilterPanel( + initialFilter: _documentsCubit.state.filter, + ), + ), + ), + isDismissible: true, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), ), ); + if (filter != null) { + _documentsCubit.updateFilter(filter: filter); + _savedViewCubit.resetSelection(); + } } Widget _buildBody(ConnectivityState connectivityState) { @@ -193,6 +165,10 @@ class _DocumentsPageState extends State { child = SliverToBoxAdapter( child: DocumentsEmptyState( state: state, + onReset: () { + _documentsCubit.updateFilter(); + _savedViewCubit.resetSelection(); + }, ), ); } @@ -201,51 +177,45 @@ class _DocumentsPageState extends State { onRefresh: _onRefresh, child: CustomScrollView( slivers: [ - BlocProvider( - create: (context) => SavedViewCubit( - RepositoryProvider.of(context)), - child: BlocListener( - listener: (context, state) { - try { - if (state.selectedSavedViewId == null) { - _documentsCubit.updateFilter(); - } else { - final newFilter = state - .value[state.selectedSavedViewId] - ?.toDocumentFilter(); - if (newFilter != null) { - _documentsCubit.updateFilter(filter: newFilter); - } + BlocListener( + listenWhen: (previous, current) => + previous.selectedSavedViewId != + current.selectedSavedViewId, + listener: (context, state) { + try { + if (state.selectedSavedViewId == null) { + _documentsCubit.updateFilter(); + } else { + final newFilter = state + .value[state.selectedSavedViewId] + ?.toDocumentFilter(); + if (newFilter != null) { + _documentsCubit.updateFilter(filter: newFilter); } - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); } - }, - child: DocumentsPageAppBar( - actions: [ - const SortDocumentsButton(), - IconButton( - icon: Icon( - settings.preferredViewType == ViewType.grid - ? Icons.list - : Icons.grid_view, - ), - onPressed: () => - BlocProvider.of( - context) - .setViewType( - settings.preferredViewType.toggle()), + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + }, + child: DocumentsPageAppBar( + actions: [ + const SortDocumentsButton(), + IconButton( + icon: Icon( + settings.preferredViewType == ViewType.grid + ? Icons.list + : Icons.grid_view, ), - ], - ), + onPressed: () => + BlocProvider.of(context) + .setViewType( + settings.preferredViewType.toggle(), + ), + ), + ], ), ), child, - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.of(context).size.height / 4, - ), - ) ], ), ); @@ -296,4 +266,31 @@ class _DocumentsPageState extends State { showErrorMessage(context, error, stackTrace); } } + + Future _loadNewPage(int pageKey) async { + final pageCount = _documentsCubit.state + .inferPageCount(pageSize: _documentsCubit.state.filter.pageSize); + if (pageCount <= pageKey + 1) { + _pagingController.nextPageKey = null; + } + try { + await _documentsCubit.loadMore(); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + void _onSelected(DocumentModel model) { + _documentsCubit.toggleDocumentSelection(model); + } + + Future _onRefresh() async { + try { + await _documentsCubit.updateCurrentFilter( + (filter) => filter.copyWith(page: 1), + ); + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } } diff --git a/lib/features/documents/view/widgets/documents_empty_state.dart b/lib/features/documents/view/widgets/documents_empty_state.dart index 971697b..a075337 100644 --- a/lib/features/documents/view/widgets/documents_empty_state.dart +++ b/lib/features/documents/view/widgets/documents_empty_state.dart @@ -9,9 +9,11 @@ import 'package:paperless_mobile/generated/l10n.dart'; class DocumentsEmptyState extends StatelessWidget { final DocumentsState state; + final VoidCallback onReset; const DocumentsEmptyState({ Key? key, required this.state, + required this.onReset, }) : super(key: key); @override @@ -22,10 +24,7 @@ class DocumentsEmptyState extends StatelessWidget { subtitle: S.of(context).documentsPageEmptyStateNothingHereText, bottomChild: state.filter != DocumentFilter.initial ? TextButton( - onPressed: () async { - await BlocProvider.of(context).updateFilter(); - BlocProvider.of(context).resetSelection(); - }, + onPressed: onReset, child: Text( S.of(context).documentsFilterPageResetFilterLabel, ), 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 e29f625..d2a619a 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -1,34 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:intl/intl.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart'; import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; -import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; -import 'package:intl/intl.dart'; import 'package:paperless_mobile/util.dart'; -import 'package:sliding_up_panel/sliding_up_panel.dart'; enum DateRangeSelection { before, after } class DocumentFilterPanel extends StatefulWidget { - final PanelController panelController; - final ScrollController scrollController; - final DocumentFilter initialFilter; - final void Function(DocumentFilter filter) onFilterChanged; const DocumentFilterPanel({ Key? key, - required this.panelController, - required this.scrollController, - required this.onFilterChanged, required this.initialFilter, }) : super(key: key); @@ -60,33 +50,17 @@ class _DocumentFilterPanelState extends State { @override Widget build(BuildContext context) { + const radius = Radius.circular(16); return ClipRRect( borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), + topLeft: radius, + topRight: radius, ), child: FormBuilder( key: _formKey, child: Column( children: [ - Stack( - alignment: Alignment.center, - children: [ - _buildDragLine(), - Align( - alignment: Alignment.topRight, - child: TextButton.icon( - icon: const Icon(Icons.refresh), - label: - Text(S.of(context).documentsFilterPageResetFilterLabel), - onPressed: () => _resetFilter(context), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), + _buildDraggableResetHeader(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -101,9 +75,6 @@ class _DocumentFilterPanelState extends State { ), ], ).padded(), - const SizedBox( - height: 16.0, - ), Expanded( child: ClipRRect( borderRadius: const BorderRadius.only( @@ -111,34 +82,30 @@ class _DocumentFilterPanelState extends State { topRight: Radius.circular(16.0), ), child: ListView( - controller: widget.scrollController, children: [ Align( alignment: Alignment.centerLeft, child: Text(S.of(context).documentsFilterPageSearchLabel), - ).padded(const EdgeInsets.only(left: 8.0)), - _buildQueryFormField(), + ).paddedOnly(left: 8.0), + _buildQueryFormField().padded(), Align( alignment: Alignment.centerLeft, child: Text(S.of(context).documentsFilterPageAdvancedLabel), - ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), - _buildCreatedDateRangePickerFormField().padded(), - _buildAddedDateRangePickerFormField().padded(), + ).padded(), + _buildCreatedDateRangePickerFormField(), + _buildAddedDateRangePickerFormField(), _buildCorrespondentFormField().padded(), _buildDocumentTypeFormField().padded(), _buildStoragePathFormField().padded(), - TagFormField( - name: DocumentModel.tagsKey, - initialValue: widget.initialFilter.tags, - allowCreation: false, - ).padded(), + _buildTagsFormField() + .paddedSymmetrically(horizontal: 8, vertical: 4.0), // Required in order for the storage path field to be visible when typing const SizedBox( height: 150, ), ], - ).padded(), + ), ), ), ], @@ -147,13 +114,39 @@ class _DocumentFilterPanelState extends State { ); } + BlocBuilder, LabelState> _buildTagsFormField() { + return BlocBuilder, LabelState>( + builder: (context, state) { + return TagFormField( + name: DocumentModel.tagsKey, + initialValue: widget.initialFilter.tags, + allowCreation: false, + selectableOptions: state.labels, + ); + }, + ); + } + + Stack _buildDraggableResetHeader() { + return Stack( + alignment: Alignment.center, + children: [ + _buildDragLine(), + Align( + alignment: Alignment.topRight, + child: TextButton.icon( + icon: const Icon(Icons.refresh), + label: Text(S.of(context).documentsFilterPageResetFilterLabel), + onPressed: () => _resetFilter(context), + ), + ), + ], + ); + } + void _resetFilter(BuildContext context) async { FocusScope.of(context).unfocus(); - await BlocProvider.of(context).updateFilter(); - BlocProvider.of(context).resetSelection(); - if (!widget.panelController.isPanelClosed) { - widget.panelController.close(); - } + Navigator.pop(context, DocumentFilter.initial); } //TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField! @@ -238,14 +231,17 @@ class _DocumentFilterPanelState extends State { ), ), initialValue: widget.initialFilter.queryText, - ).padded(); + ); } Widget _buildDateRangePickerHelper(String formFieldKey) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + const spacer = SizedBox(width: 8.0); + return SizedBox( + height: 64, + child: ListView( + scrollDirection: Axis.horizontal, children: [ + spacer, ActionChip( label: Text( S.of(context).documentsFilterPageDateRangeLastSevenDaysLabel, @@ -258,7 +254,8 @@ class _DocumentFilterPanelState extends State { ), ); }, - ).padded(const EdgeInsets.only(right: 8.0)), + ), + spacer, ActionChip( label: Text( S.of(context).documentsFilterPageDateRangeLastMonthLabel, @@ -275,7 +272,8 @@ class _DocumentFilterPanelState extends State { ), ); }, - ).padded(const EdgeInsets.only(right: 8.0)), + ), + spacer, ActionChip( label: Text( S.of(context).documentsFilterPageDateRangeLastThreeMonthsLabel, @@ -295,7 +293,8 @@ class _DocumentFilterPanelState extends State { ), ); }, - ).padded(const EdgeInsets.only(right: 8.0)), + ), + spacer, ActionChip( label: Text( S.of(context).documentsFilterPageDateRangeLastYearLabel, @@ -316,6 +315,7 @@ class _DocumentFilterPanelState extends State { ); }, ), + spacer, ], ), ); @@ -358,12 +358,12 @@ class _DocumentFilterPanelState extends State { labelText: S.of(context).documentCreatedPropertyLabel, suffixIcon: IconButton( icon: const Icon(Icons.clear), - onPressed: () => - _formKey.currentState?.fields[fkCreatedAt]?.didChange(null), + onPressed: () { + _formKey.currentState?.fields[fkCreatedAt]?.didChange(null); + }, ), ), - ), - const SizedBox(height: 4.0), + ).paddedSymmetrically(horizontal: 8, vertical: 4.0), _buildDateRangePickerHelper(fkCreatedAt), ], ); @@ -393,7 +393,7 @@ class _DocumentFilterPanelState extends State { ), child: child!, ), - format: DateFormat.yMMMd(Localizations.localeOf(context).toString()), + format: DateFormat.yMMMd(), fieldStartLabelText: S.of(context).documentsFilterPageDateRangeFieldStartLabel, fieldEndLabelText: @@ -406,11 +406,12 @@ class _DocumentFilterPanelState extends State { labelText: S.of(context).documentAddedPropertyLabel, suffixIcon: IconButton( icon: const Icon(Icons.clear), - onPressed: () => - _formKey.currentState?.fields[fkAddedAt]?.didChange(null), + onPressed: () { + _formKey.currentState?.fields[fkAddedAt]?.didChange(null); + }, ), ), - ), + ).paddedSymmetrically(horizontal: 8), const SizedBox(height: 4.0), _buildDateRangePickerHelper(fkAddedAt), ], @@ -429,28 +430,33 @@ class _DocumentFilterPanelState extends State { } void _onApplyFilter() async { - if (_formKey.currentState?.saveAndValidate() ?? false) { + _formKey.currentState?.save(); + if (_formKey.currentState?.validate() ?? false) { final v = _formKey.currentState!.value; - final docCubit = BlocProvider.of(context); - DocumentFilter newFilter = docCubit.state.filter.copyWith( + DocumentFilter newFilter = DocumentFilter( createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end, createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start, - correspondent: v[fkCorrespondent] as CorrespondentQuery?, - documentType: v[fkDocumentType] as DocumentTypeQuery?, - storagePath: v[fkStoragePath] as StoragePathQuery?, - tags: v[DocumentModel.tagsKey] as TagsQuery?, - page: 1, + correspondent: v[fkCorrespondent] as CorrespondentQuery? ?? + DocumentFilter.initial.correspondent, + documentType: v[fkDocumentType] as DocumentTypeQuery? ?? + DocumentFilter.initial.documentType, + storagePath: v[fkStoragePath] as StoragePathQuery? ?? + DocumentFilter.initial.storagePath, + tags: v[DocumentModel.tagsKey] as TagsQuery? ?? + DocumentFilter.initial.tags, queryText: v[fkQuery] as String?, addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end, addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start, queryType: v[QueryTypeFormField.fkQueryType] as QueryType, + asnQuery: widget.initialFilter.asnQuery, + page: 1, + pageSize: widget.initialFilter.pageSize, + sortField: widget.initialFilter.sortField, + sortOrder: widget.initialFilter.sortOrder, ); try { - await BlocProvider.of(context) - .updateFilter(filter: newFilter); - BlocProvider.of(context).resetSelection(); FocusScope.of(context).unfocus(); - widget.panelController.close(); + Navigator.pop(context, newFilter); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart index 43c426f..c64055e 100644 --- a/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart +++ b/lib/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class SortFieldSelectionBottomSheet extends StatefulWidget { @@ -46,30 +49,58 @@ class _SortFieldSelectionBottomSheetState S.of(context).documentsPageOrderByLabel, style: Theme.of(context).textTheme.caption, textAlign: TextAlign.start, - ).padded( - const EdgeInsets.symmetric(horizontal: 16, vertical: 16), ), TextButton( child: Text(S.of(context).documentsFilterPageApplyFilterLabel), - onPressed: () => widget.onSubmit( - _currentSortField, - _currentSortOrder, - ), + onPressed: () { + widget.onSubmit( + _currentSortField, + _currentSortOrder, + ); + Navigator.pop(context); + }, ), ], - ), + ).paddedSymmetrically(horizontal: 16, vertical: 8.0), Column( - children: SortField.values.map(_buildSortOption).toList(), + children: [ + _buildSortOption(SortField.archiveSerialNumber), + BlocBuilder, LabelState>( + builder: (context, state) { + return _buildSortOption( + SortField.correspondentName, + enabled: state.labels.values.fold( + false, + (previousValue, element) => + previousValue || (element.documentCount ?? 0) > 0), + ); + }, + ), + _buildSortOption(SortField.title), + BlocBuilder, LabelState>( + builder: (context, state) { + return _buildSortOption( + SortField.documentType, + enabled: state.labels.values.fold( + false, + (previousValue, element) => + previousValue || (element.documentCount ?? 0) > 0), + ); + }, + ), + _buildSortOption(SortField.created), + _buildSortOption(SortField.added), + _buildSortOption(SortField.modified), + ], ), ], ), ); } - Widget _buildSortOption( - SortField field, - ) { + Widget _buildSortOption(SortField field, {bool enabled = true}) { return ListTile( + enabled: enabled, contentPadding: const EdgeInsets.symmetric(horizontal: 32), title: Text( _localizedSortField(field), @@ -77,6 +108,14 @@ class _SortFieldSelectionBottomSheetState trailing: _currentSortField == field ? _buildOrderIcon(_currentSortOrder) : null, + onTap: () { + setState(() { + _currentSortOrder = (_currentSortField == field + ? _currentSortOrder.toggle() + : SortOrder.descending); + _currentSortField = field; + }); + }, ); } diff --git a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart index 6da9fb3..651b169 100644 --- a/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/documents_page_app_bar.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; @@ -35,7 +33,7 @@ class _DocumentsPageAppBarState extends State { snap: true, floating: true, pinned: true, - flexibleSpace: _buildFlexibleArea(false), + flexibleSpace: _buildFlexibleArea(false, documentsState.filter), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => @@ -56,13 +54,12 @@ class _DocumentsPageAppBarState extends State { snap: true, floating: true, pinned: true, - flexibleSpace: _buildFlexibleArea(true), - title: BlocBuilder( - builder: (context, state) { - return Text( - '${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})', - ); - }, + flexibleSpace: _buildFlexibleArea( + true, + documentsState.filter, + ), + title: Text( + '${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})', ), actions: [ ...widget.actions, @@ -73,14 +70,18 @@ class _DocumentsPageAppBarState extends State { ); } - Widget _buildFlexibleArea(bool enabled) { + Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) { return FlexibleSpaceBar( background: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - SavedViewSelectionWidget(height: 48, enabled: enabled), + SavedViewSelectionWidget( + height: 48, + enabled: enabled, + currentFilter: filter, + ), ], ), ), diff --git a/lib/features/documents/view/widgets/sort_documents_button.dart b/lib/features/documents/view/widgets/sort_documents_button.dart index 7bb61d1..04d9734 100644 --- a/lib/features/documents/view/widgets/sort_documents_button.dart +++ b/lib/features/documents/view/widgets/sort_documents_button.dart @@ -1,27 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -class SortDocumentsButton extends StatefulWidget { - const SortDocumentsButton({ - Key? key, - }) : super(key: key); +class SortDocumentsButton extends StatelessWidget { + const SortDocumentsButton({super.key}); - @override - State createState() => _SortDocumentsButtonState(); -} - -class _SortDocumentsButtonState extends State { @override Widget build(BuildContext context) { return IconButton( icon: const Icon(Icons.sort), - onPressed: _onOpenSortBottomSheet, + onPressed: () => _onOpenSortBottomSheet(context), ); } - void _onOpenSortBottomSheet() { + void _onOpenSortBottomSheet(BuildContext context) { showModalBottomSheet( elevation: 2, context: context, @@ -32,19 +28,41 @@ class _SortDocumentsButtonState extends State { topRight: Radius.circular(16), ), ), - builder: (context) => FractionallySizedBox( - heightFactor: .6, - child: BlocBuilder( - builder: (context, state) { - return SortFieldSelectionBottomSheet( - initialSortField: state.filter.sortField, - initialSortOrder: state.filter.sortOrder, - onSubmit: (field, order) => - BlocProvider.of(context).updateCurrentFilter( - (filter) => filter.copyWith(sortField: field, sortOrder: order), + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: FractionallySizedBox( + heightFactor: .6, + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>(context), + ), ), - ); - }, + BlocProvider( + create: (context) => LabelCubit( + RepositoryProvider.of>( + context), + ), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return SortFieldSelectionBottomSheet( + initialSortField: state.filter.sortField, + initialSortOrder: state.filter.sortOrder, + onSubmit: (field, order) => + BlocProvider.of(context) + .updateCurrentFilter( + (filter) => filter.copyWith( + sortField: field, + sortOrder: order, + ), + ), + ); + }, + ), + ), ), ), ); diff --git a/lib/features/edit_document/cubit/edit_document_cubit.dart b/lib/features/edit_document/cubit/edit_document_cubit.dart new file mode 100644 index 0000000..565fb0d --- /dev/null +++ b/lib/features/edit_document/cubit/edit_document_cubit.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:collection/collection.dart'; + +part 'edit_document_state.dart'; + +class EditDocumentCubit extends Cubit { + final DocumentModel _initialDocument; + final PaperlessDocumentsApi _docsApi; + + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; + final LabelRepository _storagePathRepository; + final LabelRepository _tagRepository; + + final List _subscriptions = []; + EditDocumentCubit( + DocumentModel document, { + required PaperlessDocumentsApi documentsApi, + required LabelRepository correspondentRepository, + required LabelRepository documentTypeRepository, + required LabelRepository storagePathRepository, + required LabelRepository tagRepository, + }) : _initialDocument = document, + _docsApi = documentsApi, + _correspondentRepository = correspondentRepository, + _documentTypeRepository = documentTypeRepository, + _storagePathRepository = storagePathRepository, + _tagRepository = tagRepository, + super( + EditDocumentState( + document: document, + correspondents: correspondentRepository.current, + documentTypes: documentTypeRepository.current, + storagePaths: storagePathRepository.current, + tags: tagRepository.current, + ), + ) { + _subscriptions.add( + _correspondentRepository.labels + .listen((v) => emit(state.copyWith(correspondents: v))), + ); + _subscriptions.add( + _documentTypeRepository.labels + .listen((v) => emit(state.copyWith(documentTypes: v))), + ); + _subscriptions.add( + _storagePathRepository.labels + .listen((v) => emit(state.copyWith(storagePaths: v))), + ); + _subscriptions.add( + _tagRepository.labels.listen( + (v) => emit(state.copyWith(tags: v)), + ), + ); + } + + Future updateDocument(DocumentModel document) async { + final updated = await _docsApi.update(document); + // Reload changed labels (documentCount property changes with removal/add) + if (document.documentType != _initialDocument.documentType) { + _documentTypeRepository + .find((document.documentType ?? _initialDocument.documentType)!); + } + if (document.correspondent != _initialDocument.correspondent) { + _correspondentRepository + .find((document.correspondent ?? _initialDocument.correspondent)!); + } + if (document.storagePath != _initialDocument.storagePath) { + _storagePathRepository + .find((document.storagePath ?? _initialDocument.storagePath)!); + } + if (!const DeepCollectionEquality.unordered() + .equals(document.tags, _initialDocument.tags)) { + _tagRepository.findAll(document.tags); + } + emit(state.copyWith(document: updated)); + } + + @override + Future close() { + for (final sub in _subscriptions) { + sub.cancel(); + } + return super.close(); + } +} diff --git a/lib/features/edit_document/cubit/edit_document_state.dart b/lib/features/edit_document/cubit/edit_document_state.dart new file mode 100644 index 0000000..ea19cd1 --- /dev/null +++ b/lib/features/edit_document/cubit/edit_document_state.dart @@ -0,0 +1,43 @@ +part of 'edit_document_cubit.dart'; + +class EditDocumentState extends Equatable { + final DocumentModel document; + + final Map correspondents; + final Map documentTypes; + final Map storagePaths; + final Map tags; + + const EditDocumentState({ + required this.correspondents, + required this.documentTypes, + required this.storagePaths, + required this.tags, + required this.document, + }); + + @override + List get props => [ + correspondents, + documentTypes, + storagePaths, + tags, + document, + ]; + + EditDocumentState copyWith({ + Map? correspondents, + Map? documentTypes, + Map? storagePaths, + Map? tags, + DocumentModel? document, + }) { + return EditDocumentState( + document: document ?? this.document, + correspondents: correspondents ?? this.correspondents, + documentTypes: documentTypes ?? this.documentTypes, + storagePaths: storagePaths ?? this.storagePaths, + tags: tags ?? this.tags, + ); + } +} diff --git a/lib/features/edit_label/cubit/edit_label_cubit.dart b/lib/features/edit_label/cubit/edit_label_cubit.dart index 39ec63b..cb75902 100644 --- a/lib/features/edit_label/cubit/edit_label_cubit.dart +++ b/lib/features/edit_label/cubit/edit_label_cubit.dart @@ -17,9 +17,9 @@ class EditLabelCubit extends Cubit> { .listen((labels) => emit(EditLabelState(labels: labels))); } - Future create(T label) => _repository.create(label); + Future create(T label) => _repository.create(label); - Future update(T label) => _repository.update(label); + Future update(T label) => _repository.update(label); Future delete(T label) => _repository.delete(label); diff --git a/lib/features/edit_label/view/add_label_page.dart b/lib/features/edit_label/view/add_label_page.dart index 25e71d6..973bd66 100644 --- a/lib/features/edit_label/view/add_label_page.dart +++ b/lib/features/edit_label/view/add_label_page.dart @@ -28,7 +28,7 @@ class AddLabelPage extends StatelessWidget { ), child: AddLabelFormWidget( pageTitle: pageTitle, - label: fromJsonT({'name': initialName}), + label: initialName != null ? fromJsonT({'name': initialName}) : null, additionalFields: additionalFields, fromJsonT: fromJsonT, ), diff --git a/lib/features/edit_label/view/edit_label_page.dart b/lib/features/edit_label/view/edit_label_page.dart index 5e3ac0b..22374a7 100644 --- a/lib/features/edit_label/view/edit_label_page.dart +++ b/lib/features/edit_label/view/edit_label_page.dart @@ -70,33 +70,38 @@ class EditLabelForm extends StatelessWidget { ); } - void _onDelete(BuildContext context) { + void _onDelete(BuildContext context) async { if ((label.documentCount ?? 0) > 0) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle), - content: Text( - S.of(context).editLabelPageDeletionDialogText, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(S.of(context).genericActionCancelLabel), - ), - TextButton( - onPressed: () { - BlocProvider.of>(context).delete(label); - Navigator.pop(context); - }, - child: Text( - S.of(context).genericActionDeleteLabel, - style: TextStyle(color: Theme.of(context).errorColor), + final shouldDelete = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: + Text(S.of(context).editLabelPageConfirmDeletionDialogTitle), + content: Text( + S.of(context).editLabelPageDeletionDialogText, ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(S.of(context).genericActionCancelLabel), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text( + S.of(context).genericActionDeleteLabel, + style: TextStyle(color: Theme.of(context).errorColor), + ), + ), + ], ), - ], - ), - ); + ) ?? + false; + if (shouldDelete) { + BlocProvider.of>(context).delete(label); + Navigator.pop(context); + } } else { BlocProvider.of>(context).delete(label); Navigator.pop(context); 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 7556027..e4448af 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 @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; class EditDocumentTypePage extends StatelessWidget { final DocumentType documentType; @@ -12,7 +12,7 @@ class EditDocumentTypePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LabelCubit( + create: (context) => EditLabelCubit( RepositoryProvider.of>(context), ), child: EditLabelPage( 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 abc9f04..1dcebe8 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 @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; class EditStoragePathPage extends StatelessWidget { @@ -13,7 +13,7 @@ class EditStoragePathPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LabelCubit( + create: (context) => EditLabelCubit( RepositoryProvider.of>(context), ), child: EditLabelPage( 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 97601e2..b94ceea 100644 --- a/lib/features/edit_label/view/impl/edit_tag_page.dart +++ b/lib/features/edit_label/view/impl/edit_tag_page.dart @@ -4,8 +4,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; +import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class EditTagPage extends StatelessWidget { @@ -16,7 +16,7 @@ class EditTagPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LabelCubit( + create: (context) => EditLabelCubit( RepositoryProvider.of>(context), ), child: EditLabelPage( diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index f84cc9f..0b319ff 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -10,7 +10,7 @@ import 'package:paperless_mobile/util.dart'; class SubmitButtonConfig { final Widget icon; final Widget label; - final Future Function(T) onSubmit; + final Future Function(T) onSubmit; SubmitButtonConfig({ required this.icon, @@ -117,8 +117,9 @@ class _LabelFormState extends State> { ...widget.initialValue?.toJson() ?? {}, ..._formKey.currentState!.value }; - await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson)); - Navigator.pop(context); + final createdLabel = await widget.submitButtonConfig + .onSubmit(widget.fromJsonT(mergedJson)); + Navigator.pop(context, createdLabel); } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); } on PaperlessServerException catch (error, stackTrace) { diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index a699c30..e8092c3 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -59,6 +59,11 @@ class _HomePageState extends State { BlocProvider.value( value: DocumentsCubit(getIt()), ), + BlocProvider( + create: (context) => SavedViewCubit( + RepositoryProvider.of(context), + ), + ), ], child: const DocumentsPage(), ), diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index 00ec9a6..f8ae721 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -8,11 +8,9 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; -import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; @@ -52,7 +50,7 @@ class InfoDrawer extends StatelessWidget { height: 32, width: 32, color: Theme.of(context).colorScheme.onPrimaryContainer, - ).padded(const EdgeInsets.only(right: 8.0)), + ).paddedOnly(right: 8.0), Text( S.of(context).appTitleText, style: Theme.of(context).textTheme.headline5?.copyWith( @@ -215,7 +213,7 @@ class InfoDrawer extends StatelessWidget { create: (context) => InboxCubit( RepositoryProvider.of>(context), getIt(), - ), + )..loadInbox(), child: const InboxPage(), ), ), diff --git a/lib/features/inbox/view/pages/inbox_page.dart b/lib/features/inbox/view/pages/inbox_page.dart index 951897f..4dfaef2 100644 --- a/lib/features/inbox/view/pages/inbox_page.dart +++ b/lib/features/inbox/view/pages/inbox_page.dart @@ -54,12 +54,12 @@ class _InboxPageState extends State { '${state.inboxItems.length} ${S.of(context).inboxPageUnseenText}', textAlign: TextAlign.start, style: Theme.of(context).textTheme.caption, - ).padded(const EdgeInsets.symmetric(horizontal: 4.0)), + ).paddedSymmetrically(horizontal: 4.0), ), ), ); }, - ).padded(const EdgeInsets.symmetric(horizontal: 8.0)), + ).paddedSymmetrically(horizontal: 8.0), ), ), floatingActionButton: BlocBuilder( @@ -108,7 +108,7 @@ class _InboxPageState extends State { textAlign: TextAlign.center, ).padded(), ), - ).padded(const EdgeInsets.only(top: 8.0)), + ).paddedOnly(top: 8.0), ), SliverList( delegate: SliverChildBuilderDelegate( @@ -137,14 +137,7 @@ class _InboxPageState extends State { S.of(context).inboxPageUsageHintText, textAlign: TextAlign.center, style: Theme.of(context).textTheme.caption, - ).padded( - const EdgeInsets.only( - top: 8.0, - left: 8.0, - right: 8.0, - bottom: 8.0, - ), - ), + ).padded(), ), ...slivers ], diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart index bee3ae1..859d13c 100644 --- a/lib/features/labels/bloc/label_cubit.dart +++ b/lib/features/labels/bloc/label_cubit.dart @@ -11,9 +11,13 @@ class LabelCubit extends Cubit> { late StreamSubscription _subscription; - LabelCubit(this._repository) : super(LabelState.initial()) { + LabelCubit(LabelRepository repository) + : _repository = repository, + super(LabelState(labels: repository.current, isLoaded: true)) { _subscription = _repository.labels.listen( - (update) => emit(LabelState(isLoaded: true, labels: update)), + (update) => emit( + LabelState(isLoaded: true, labels: update), + ), ); } @@ -28,6 +32,10 @@ class LabelCubit extends Cubit> { return addedItem; } + Future reload() { + return _repository.findAll(); + } + Future replace(T item) async { assert(item.id != null); final updatedItem = await _repository.update(item); 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 dbe7cf1..a2d8bcf 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -4,11 +4,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; -import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; -import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart'; import 'package:paperless_mobile/generated/l10n.dart'; class TagFormField extends StatefulWidget { @@ -18,6 +14,7 @@ class TagFormField extends StatefulWidget { final bool notAssignedSelectable; final bool anyAssignedSelectable; final bool excludeAllowed; + final Map selectableOptions; const TagFormField({ super.key, @@ -27,6 +24,7 @@ class TagFormField extends StatefulWidget { this.notAssignedSelectable = true, this.anyAssignedSelectable = true, this.excludeAllowed = true, + required this.selectableOptions, }); @override @@ -47,10 +45,7 @@ class _TagFormFieldState extends State { _textEditingController = TextEditingController() ..addListener(() { setState(() { - _showCreationSuffixIcon = BlocProvider.of>(context) - .state - .labels - .values + _showCreationSuffixIcon = widget.selectableOptions.values .where( (item) => item.name.toLowerCase().startsWith( _textEditingController.text.toLowerCase(), @@ -66,122 +61,126 @@ class _TagFormFieldState extends State { @override Widget build(BuildContext context) { - return TagBlocProvider( - child: BlocBuilder, LabelState>( - builder: (context, tagState) { - return FormBuilderField( - builder: (field) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TypeAheadField( - textFieldConfiguration: TextFieldConfiguration( - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.label_outline, - ), - suffixIcon: _buildSuffixIcon(context, field), - labelText: S.of(context).documentTagsPropertyLabel, - hintText: S.of(context).tagFormFieldSearchHintText, - ), - controller: _textEditingController, - ), - suggestionsCallback: (query) { - final suggestions = tagState.labels.values - .where((element) => element.name - .toLowerCase() - .startsWith(query.toLowerCase())) - .map((e) => e.id!) - .toList(); - if (field.value is IdsTagsQuery) { - suggestions.removeWhere((element) => - (field.value as IdsTagsQuery) - .ids - .contains(element)); - } - if (widget.notAssignedSelectable && - field.value is! OnlyNotAssignedTagsQuery) { - suggestions.insert(0, _onlyNotAssignedId); - } - if (widget.anyAssignedSelectable && - field.value is! AnyAssignedTagsQuery) { - suggestions.insert(0, _anyAssignedId); - } - return suggestions; - }, - getImmediateSuggestions: true, - animationStart: 1, - itemBuilder: (context, data) { - if (data == _onlyNotAssignedId) { - return ListTile( - title: Text(S.of(context).labelNotAssignedText), - ); - } else if (data == _anyAssignedId) { - return ListTile( - title: Text(S.of(context).labelAnyAssignedText), - ); - } - final tag = tagState.getLabel(data)!; - return ListTile( - leading: Icon( - Icons.circle, - color: tag.color, - ), - title: Text( - tag.name, - style: TextStyle( - color: - Theme.of(context).colorScheme.onBackground), - ), - ); - }, - onSuggestionSelected: (id) { - if (id == _onlyNotAssignedId) { - //Not assigned tag - field.didChange(const OnlyNotAssignedTagsQuery()); - return; - } else if (id == _anyAssignedId) { - field.didChange(const AnyAssignedTagsQuery()); - } else { - final tagsQuery = field.value is IdsTagsQuery - ? field.value as IdsTagsQuery - : const IdsTagsQuery(); - field.didChange(tagsQuery - .withIdQueriesAdded([IncludeTagIdQuery(id)])); - } - _textEditingController.clear(); - }, - direction: AxisDirection.up, + final isEnabled = widget.selectableOptions.values.fold( + false, + (previousValue, element) => + previousValue || (element.documentCount ?? 0) > 0) || + widget.allowCreation; + + return FormBuilderField( + enabled: isEnabled, + builder: (field) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TypeAheadField( + textFieldConfiguration: TextFieldConfiguration( + enabled: isEnabled, + decoration: InputDecoration( + prefixIcon: const Icon( + Icons.label_outline, ), - if (field.value is OnlyNotAssignedTagsQuery) ...[ - _buildNotAssignedTag(field) - ] else if (field.value is AnyAssignedTagsQuery) ...[ - _buildAnyAssignedTag(field) - ] else ...[ - // field.value is IdsTagsQuery - Wrap( - alignment: WrapAlignment.start, - runAlignment: WrapAlignment.start, - spacing: 8.0, - children: ((field.value as IdsTagsQuery).queries) - .map( - (query) => _buildTag( - field, - query, - tagState.getLabel(query.id), - ), - ) - .toList(), - ), - ] - ], - ); - }, - initialValue: widget.initialValue ?? const IdsTagsQuery(), - name: widget.name, - ); - }, - ), + suffixIcon: _buildSuffixIcon(context, field), + labelText: S.of(context).documentTagsPropertyLabel, + hintText: S.of(context).tagFormFieldSearchHintText, + ), + controller: _textEditingController, + ), + suggestionsCallback: (query) { + final suggestions = widget.selectableOptions.entries + .where( + (entry) => entry.value.name + .toLowerCase() + .startsWith(query.toLowerCase()), + ) + .where((entry) => + widget.allowCreation || + (entry.value.documentCount ?? 0) > 0) + .map((entry) => entry.key) + .toList(); + if (field.value is IdsTagsQuery) { + suggestions.removeWhere((element) => + (field.value as IdsTagsQuery).ids.contains(element)); + } + if (widget.notAssignedSelectable && + field.value is! OnlyNotAssignedTagsQuery) { + suggestions.insert(0, _onlyNotAssignedId); + } + if (widget.anyAssignedSelectable && + field.value is! AnyAssignedTagsQuery) { + suggestions.insert(0, _anyAssignedId); + } + return suggestions; + }, + getImmediateSuggestions: true, + animationStart: 1, + itemBuilder: (context, data) { + if (data == _onlyNotAssignedId) { + return ListTile( + title: Text(S.of(context).labelNotAssignedText), + ); + } else if (data == _anyAssignedId) { + return ListTile( + title: Text(S.of(context).labelAnyAssignedText), + ); + } + final tag = widget.selectableOptions[data]!; + return ListTile( + leading: Icon( + Icons.circle, + color: tag.color, + ), + title: Text( + tag.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground), + ), + ); + }, + onSuggestionSelected: (id) { + if (id == _onlyNotAssignedId) { + //Not assigned tag + field.didChange(const OnlyNotAssignedTagsQuery()); + return; + } else if (id == _anyAssignedId) { + field.didChange(const AnyAssignedTagsQuery()); + } else { + final tagsQuery = field.value is IdsTagsQuery + ? field.value as IdsTagsQuery + : const IdsTagsQuery(); + field.didChange( + tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(id)])); + } + _textEditingController.clear(); + }, + direction: AxisDirection.up, + ), + if (field.value is OnlyNotAssignedTagsQuery) ...[ + _buildNotAssignedTag(field) + ] else if (field.value is AnyAssignedTagsQuery) ...[ + _buildAnyAssignedTag(field) + ] else ...[ + // field.value is IdsTagsQuery + Wrap( + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + spacing: 8.0, + children: ((field.value as IdsTagsQuery).queries) + .map( + (query) => _buildTag( + field, + query, + widget.selectableOptions[query.id], + ), + ) + .toList(), + ), + ] + ], + ); + }, + initialValue: widget.initialValue ?? const IdsTagsQuery(), + name: widget.name, ); } diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index c3e9ffc..e0dbe07 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -30,6 +30,7 @@ class _LabelsPageState extends State @override void initState() { super.initState(); + _tabController = TabController(length: 4, vsync: this) ..addListener(() => setState(() => _currentIndex = _tabController.index)); } diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index 4b1aca8..76d5273 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -55,13 +57,15 @@ class _LabelFormFieldState super.initState(); _showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id); _textEditingController = TextEditingController( - text: widget.state[widget.initialValue?.id]?.name ?? '') - ..addListener(() { + text: widget.state[widget.initialValue?.id]?.name ?? '', + )..addListener(() { setState(() { _showCreationSuffixIcon = widget.state.values - .where((item) => item.name.toLowerCase().startsWith( - _textEditingController.text.toLowerCase(), - )) + .where( + (item) => item.name.toLowerCase().startsWith( + _textEditingController.text.toLowerCase(), + ), + ) .isEmpty; }); setState(() => @@ -71,7 +75,13 @@ class _LabelFormFieldState @override Widget build(BuildContext context) { + final isEnabled = widget.state.values.fold( + false, + (previousValue, element) => + previousValue || (element.documentCount ?? 0) > 0) || + widget.labelCreationWidgetBuilder != null; return FormBuilderTypeAhead( + enabled: isEnabled, noItemsFoundBuilder: (context) => Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( @@ -88,13 +98,20 @@ class _LabelFormFieldState S.of(context).labelNotAssignedText), ), suggestionsCallback: (pattern) { - final List suggestions = widget.state.keys - .where((item) => - widget.state[item]!.name - .toLowerCase() - .startsWith(pattern.toLowerCase()) || - pattern.isEmpty) - .map((id) => widget.queryParameterIdBuilder(id)) + final List suggestions = widget.state.entries + .where( + (entry) => + widget.state[entry.key]!.name + .toLowerCase() + .contains(pattern.toLowerCase()) || + pattern.isEmpty, + ) + .where( + (entry) => + widget.labelCreationWidgetBuilder != null || + (entry.value.documentCount ?? 0) > 0, + ) + .map((entry) => widget.queryParameterIdBuilder(entry.key)) .toList(); if (widget.notAssignedSelectable) { suggestions.insert(0, widget.queryParameterNotAssignedBuilder()); @@ -128,21 +145,23 @@ class _LabelFormFieldState Widget? _buildSuffixIcon(BuildContext context) { if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) { return IconButton( - onPressed: () => Navigator.of(context) - .push(MaterialPageRoute( - builder: (context) => widget - .labelCreationWidgetBuilder!(_textEditingController.text))) - .then((value) { - if (value != null) { + onPressed: () async { + FocusScope.of(context).unfocus(); + final createdLabel = await showDialog( + context: context, + builder: (context) => widget.labelCreationWidgetBuilder!( + _textEditingController.text, + ), + ); + if (createdLabel != null) { // If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done). widget.formBuilderState?.fields[widget.name] - ?.didChange(widget.queryParameterIdBuilder(value.id)); - _textEditingController.text = value.name; - FocusScope.of(context).unfocus(); + ?.didChange(widget.queryParameterIdBuilder(createdLabel.id)); + _textEditingController.text = createdLabel.name; } else { _reset(); } - }), + }, icon: const Icon( Icons.new_label, ), @@ -158,8 +177,9 @@ class _LabelFormFieldState } void _reset() { - widget.formBuilderState?.fields[widget.name] - ?.didChange(widget.queryParameterIdBuilder(null)); + widget.formBuilderState?.fields[widget.name]?.didChange( + widget.queryParameterIdBuilder(null), // equivalnt to IdQueryParam.unset() + ); _textEditingController.clear(); } @@ -169,9 +189,7 @@ class _LabelFormFieldState } else if (T == DocumentType) { return S.of(context).documentTypeFormFieldSearchHintText; } else { - return S - .of(context) - .tagFormFieldSearchHintText; //TODO: Update tag form field once there is multi selection support. + return S.of(context).tagFormFieldSearchHintText; } } } diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 8821269..1212a02 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -62,18 +62,21 @@ class LabelTabView extends StatelessWidget { ), ); } - return ListView( - children: labels - .map((l) => LabelItem( - name: l.name, - content: - contentBuilder?.call(l) ?? Text(l.match ?? '-'), - onOpenEditPage: onEdit, - filterBuilder: filterBuilder, - leading: leadingBuilder?.call(l), - label: l, - )) - .toList(), + return RefreshIndicator( + onRefresh: BlocProvider.of>(context).reload, + child: ListView( + children: labels + .map((l) => LabelItem( + name: l.name, + content: + contentBuilder?.call(l) ?? Text(l.match ?? '-'), + onOpenEditPage: onEdit, + filterBuilder: filterBuilder, + leading: leadingBuilder?.call(l), + label: l, + )) + .toList(), + ), ); }, ); diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index a51fc9b..e63d6a5 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -81,7 +81,8 @@ class AuthenticationCubit extends Cubit { appSettings = ApplicationSettingsState.defaultSettings; } if (storedAuth == null || !storedAuth.isValid) { - emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); + return emit( + AuthenticationState(isAuthenticated: false, wasLoginStored: false)); } else { if (appSettings.isLocalAuthenticationEnabled) { final localAuthSuccess = await _localAuthService @@ -103,8 +104,13 @@ class AuthenticationCubit extends Cubit { wasLocalAuthenticationSuccessful: false, )); } + } else { + return emit(AuthenticationState( + isAuthenticated: true, + authentication: storedAuth, + wasLoginStored: true, + )); } - emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true)); } } diff --git a/lib/features/saved_view/view/saved_view_selection_widget.dart b/lib/features/saved_view/view/saved_view_selection_widget.dart index 18c7eaa..6ce9e58 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -1,20 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; +import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/util.dart'; class SavedViewSelectionWidget extends StatelessWidget { + final DocumentFilter currentFilter; const SavedViewSelectionWidget({ Key? key, required this.height, required this.enabled, + required this.currentFilter, }) : super(key: key); final double height; @@ -64,10 +65,18 @@ class SavedViewSelectionWidget extends StatelessWidget { S.of(context).savedViewsLabel, style: Theme.of(context).textTheme.titleSmall, ), - TextButton.icon( - icon: const Icon(Icons.add), - onPressed: enabled ? () => _onCreatePressed(context) : null, - label: Text(S.of(context).savedViewCreateNewLabel), + BlocBuilder( + buildWhen: (previous, current) => + previous.filter != current.filter, + builder: (context, docState) { + return TextButton.icon( + icon: const Icon(Icons.add), + onPressed: enabled + ? () => _onCreatePressed(context, docState.filter) + : null, + label: Text(S.of(context).savedViewCreateNewLabel), + ); + }, ), ], ), @@ -75,11 +84,11 @@ class SavedViewSelectionWidget extends StatelessWidget { ); } - void _onCreatePressed(BuildContext context) async { + void _onCreatePressed(BuildContext context, DocumentFilter filter) async { final newView = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => AddSavedViewPage( - currentFilter: getIt().state.filter, + currentFilter: filter, ), ), ); diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index e67ad46..5a8feeb 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -107,6 +107,7 @@ class _ScannerPageState extends State if (kDebugMode) { dev.log('[ScannerPage] Created temporary file: ${file.path}'); } + final success = await EdgeDetection.detectEdge(file.path); if (!success) { if (kDebugMode) { diff --git a/lib/main.dart b/lib/main.dart index 2091935..bbd6ccc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -120,6 +120,10 @@ class _PaperlessMobileEntrypointState extends State { ), inputDecorationTheme: const InputDecorationTheme( border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + ), ), chipTheme: ChipThemeData( backgroundColor: Colors.lightGreen[50], @@ -134,8 +138,11 @@ class _PaperlessMobileEntrypointState extends State { scrolledUnderElevation: 0.0, ), inputDecorationTheme: const InputDecorationTheme( - border: OutlineInputBorder(), - ), + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16.0, + )), chipTheme: ChipThemeData( backgroundColor: Colors.green[900], ), diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index e24de6f..14651d2 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -25,7 +25,7 @@ class DocumentFilter extends Equatable { final DocumentTypeQuery documentType; final CorrespondentQuery correspondent; final StoragePathQuery storagePath; - final AsnQuery asn; + final AsnQuery asnQuery; final TagsQuery tags; final SortField sortField; final SortOrder sortOrder; @@ -42,7 +42,7 @@ class DocumentFilter extends Equatable { this.documentType = const DocumentTypeQuery.unset(), this.correspondent = const CorrespondentQuery.unset(), this.storagePath = const StoragePathQuery.unset(), - this.asn = const AsnQuery.unset(), + this.asnQuery = const AsnQuery.unset(), this.tags = const IdsTagsQuery(), this.sortField = SortField.created, this.sortOrder = SortOrder.descending, @@ -60,7 +60,7 @@ class DocumentFilter extends Equatable { sb.write(correspondent.toQueryParameter()); sb.write(tags.toQueryParameter()); sb.write(storagePath.toQueryParameter()); - sb.write(asn.toQueryParameter()); + sb.write(asnQuery.toQueryParameter()); if (queryText?.isNotEmpty ?? false) { sb.write("&${queryType.queryParam}=$queryText"); @@ -104,6 +104,7 @@ class DocumentFilter extends Equatable { DocumentTypeQuery? documentType, CorrespondentQuery? correspondent, StoragePathQuery? storagePath, + AsnQuery? asnQuery, TagsQuery? tags, SortField? sortField, SortOrder? sortOrder, @@ -129,6 +130,7 @@ class DocumentFilter extends Equatable { queryText: queryText ?? this.queryText, createdDateBefore: createdDateBefore ?? this.createdDateBefore, createdDateAfter: createdDateAfter ?? this.createdDateAfter, + asnQuery: asnQuery ?? this.asnQuery, ); } @@ -153,6 +155,19 @@ class DocumentFilter extends Equatable { return null; } + int get appliedFiltersCount => [ + documentType != initial.documentType, + correspondent != initial.correspondent, + storagePath != initial.storagePath, + tags != initial.tags, + (addedDateAfter != initial.addedDateAfter || + addedDateBefore != initial.addedDateBefore), + (createdDateAfter != initial.createdDateAfter || + createdDateBefore != initial.createdDateBefore), + asnQuery != initial.asnQuery, + (queryType != initial.queryType || queryText != initial.queryText), + ].fold(0, (previousValue, element) => previousValue += element ? 1 : 0); + @override List get props => [ pageSize, @@ -160,7 +175,7 @@ class DocumentFilter extends Equatable { documentType, correspondent, storagePath, - asn, + asnQuery, tags, sortField, sortOrder, diff --git a/packages/paperless_api/lib/src/models/document_model.dart b/packages/paperless_api/lib/src/models/document_model.dart index ad55482..ab2e7f7 100644 --- a/packages/paperless_api/lib/src/models/document_model.dart +++ b/packages/paperless_api/lib/src/models/document_model.dart @@ -51,9 +51,9 @@ class DocumentModel extends Equatable { : id = json[idKey], title = json[titleKey], content = json[contentKey], - created = DateTime.parse(json[createdKey]), - modified = DateTime.parse(json[modifiedKey]), - added = DateTime.parse(json[addedKey]), + created = DateTime.parse(json[createdKey]).toLocal(), + modified = DateTime.parse(json[modifiedKey]).toLocal(), + added = DateTime.parse(json[addedKey]).toLocal(), archiveSerialNumber = json[asnKey], originalFileName = json[originalFileNameKey], archivedFileName = json[archivedFileNameKey], @@ -71,9 +71,9 @@ class DocumentModel extends Equatable { contentKey: content, correspondentKey: correspondent, documentTypeKey: documentType, - createdKey: created.toUtc().toIso8601String(), - modifiedKey: modified.toUtc().toIso8601String(), - addedKey: added.toUtc().toIso8601String(), + createdKey: created.toIso8601String(), + modifiedKey: modified.toIso8601String(), + addedKey: added.toIso8601String(), originalFileNameKey: originalFileName, tagsKey: tags.toList(), storagePathKey: storagePath, diff --git a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart index 11e6890..b7bd2f6 100644 --- a/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/documents_api/paperless_documents_api_impl.dart @@ -187,7 +187,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi { const DocumentFilter asnQueryFilter = DocumentFilter( sortField: SortField.archiveSerialNumber, sortOrder: SortOrder.descending, - asn: AsnQuery.anyAssigned(), + asnQuery: AsnQuery.anyAssigned(), page: 1, pageSize: 1, ); diff --git a/pubspec.lock b/pubspec.lock index c0564f8..b87081e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.4.0" async: dependency: transitive description: @@ -43,13 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.9.0" + badges: + dependency: "direct main" + description: + name: badges + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" barcode: dependency: transitive description: name: barcode url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.3" bloc: dependency: transitive description: @@ -77,14 +84,14 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: @@ -98,21 +105,21 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.11" + version: "2.3.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.3" + version: "7.2.7" built_collection: dependency: transitive description: @@ -126,28 +133,28 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.3.2" + version: "8.4.2" cached_network_image: dependency: "direct main" description: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: @@ -210,7 +217,7 @@ packages: name: connectivity_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.3" connectivity_plus_web: dependency: transitive description: @@ -231,7 +238,7 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.1" coverage: dependency: transitive description: @@ -266,7 +273,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.3" + version: "2.2.4" dbus: dependency: transitive description: @@ -343,13 +350,13 @@ packages: name: dropdown_search url: "https://pub.dartlang.org" source: hosted - version: "5.0.3" + version: "5.0.5" edge_detection: dependency: "direct main" description: path: "." ref: master - resolved-ref: "19fbebef99360e9cf0b59c6a90ff7cd26d4d6e7d" + resolved-ref: "2d417dd77e075cb12e82a390e50cc4554e877ec4" url: "https://github.com/sawankumarbundelkhandi/edge_detection" source: git version: "1.1.1" @@ -366,7 +373,7 @@ packages: name: encrypted_shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" equatable: dependency: "direct main" description: @@ -474,7 +481,7 @@ packages: name: flutter_form_builder url: "https://pub.dartlang.org" source: hosted - version: "7.5.0" + version: "7.7.0" flutter_keyboard_visibility: dependency: transitive description: @@ -535,14 +542,14 @@ packages: name: flutter_native_splash url: "https://pub.dartlang.org" source: hosted - version: "2.2.11" + version: "2.2.16" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" flutter_rating_bar: dependency: transitive description: @@ -556,7 +563,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.1+1" + version: "1.1.6" flutter_test: dependency: "direct dev" description: flutter @@ -587,20 +594,20 @@ packages: name: fluttertoast url: "https://pub.dartlang.org" source: hosted - version: "8.1.1" + version: "8.1.2" font_awesome_flutter: dependency: "direct main" description: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "10.1.0" + version: "10.3.0" form_builder_extra_fields: dependency: "direct main" description: path: "." ref: main - resolved-ref: "33ba0a4407086275ac4357badc631be550fb3bcc" + resolved-ref: b02de7dad9c00ece575ad4b8dfba73a3e1239e9c url: "https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git" source: git version: "8.4.0" @@ -636,14 +643,14 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" hive: dependency: "direct main" description: @@ -657,7 +664,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -671,28 +678,28 @@ packages: name: http_interceptor url: "https://pub.dartlang.org" source: hosted - version: "2.0.0-beta.5" + version: "2.0.0-beta.6" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" image: dependency: "direct main" description: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.2" infinite_scroll_pagination: dependency: "direct main" description: @@ -713,7 +720,7 @@ packages: name: injectable_generator url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" integration_test: dependency: "direct dev" description: flutter @@ -739,7 +746,7 @@ packages: name: introduction_screen url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.1" io: dependency: transitive description: @@ -781,7 +788,7 @@ packages: name: local_auth_android url: "https://pub.dartlang.org" source: hosted - version: "1.0.13" + version: "1.0.15" local_auth_ios: dependency: transitive description: @@ -809,7 +816,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" matcher: dependency: transitive description: @@ -837,7 +844,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" mockito: dependency: "direct dev" description: @@ -886,7 +893,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -921,7 +928,7 @@ packages: name: package_info_plus_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.6" package_info_plus_windows: dependency: transitive description: @@ -949,14 +956,14 @@ packages: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" path_provider: dependency: "direct main" description: @@ -970,14 +977,14 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.0.22" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.11" path_provider_linux: dependency: transitive description: @@ -998,7 +1005,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: @@ -1012,7 +1019,7 @@ packages: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "3.8.1" + version: "3.8.4" pdfx: dependency: "direct main" description: @@ -1047,28 +1054,28 @@ packages: name: permission_handler_apple url: "https://pub.dartlang.org" source: hosted - version: "9.0.4" + version: "9.0.7" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.7.0" + version: "3.9.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.2" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.0" photo_view: dependency: "direct main" description: @@ -1089,21 +1096,21 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.6.0" + version: "3.6.2" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: @@ -1117,21 +1124,21 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.4" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" qr: dependency: transitive description: @@ -1139,6 +1146,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" receive_sharing_intent: dependency: "direct main" description: @@ -1159,7 +1173,7 @@ packages: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "6.2.0" + version: "6.3.0" share_plus_platform_interface: dependency: transitive description: @@ -1180,7 +1194,7 @@ packages: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.14" shared_preferences_ios: dependency: transitive description: @@ -1208,7 +1222,7 @@ packages: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_web: dependency: transitive description: @@ -1229,7 +1243,7 @@ packages: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: @@ -1250,7 +1264,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" shimmer: dependency: "direct main" description: @@ -1264,47 +1278,40 @@ packages: name: signature url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "5.3.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - sliding_up_panel: - dependency: "direct main" - description: - name: sliding_up_panel - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0+1" sliver_tools: dependency: transitive description: name: sliver_tools url: "https://pub.dartlang.org" source: hosted - version: "0.2.7" + version: "0.2.8" source_gen: dependency: transitive description: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.6" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.10" + version: "0.10.11" source_span: dependency: transitive description: @@ -1318,14 +1325,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2+1" + version: "2.2.2" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1+1" + version: "2.4.0+2" stack_trace: dependency: transitive description: @@ -1346,7 +1353,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1367,7 +1374,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0+2" + version: "3.0.0+3" term_glyph: dependency: transitive description: @@ -1430,14 +1437,14 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.0.22" url_launcher_ios: dependency: transitive description: @@ -1486,7 +1493,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1507,7 +1514,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: "direct main" description: @@ -1535,14 +1542,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.2" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+1" + version: "0.2.0+2" xml: dependency: transitive description: @@ -1559,4 +1566,4 @@ packages: version: "3.1.1" sdks: dart: ">=2.18.5 <3.0.0" - flutter: ">=3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6215f45..d7c82cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,6 @@ dependencies: ref: main form_builder_validators: ^8.4.0 infinite_scroll_pagination: ^3.2.0 - sliding_up_panel: ^2.0.0+1 package_info_plus: ^1.4.3+1 font_awesome_flutter: ^10.1.0 local_auth: ^2.1.2 @@ -82,6 +81,7 @@ dependencies: path: packages/paperless_api hive: ^2.2.3 rxdart: ^0.27.7 + badges: ^2.0.3 dev_dependencies: integration_test: diff --git a/test/src/bloc/document_cubit_test.dart b/test/src/bloc/document_cubit_test.dart index 812318b..67c4c5d 100644 --- a/test/src/bloc/document_cubit_test.dart +++ b/test/src/bloc/document_cubit_test.dart @@ -1,7 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; -import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart';