From 4bf4ff1cbde108ed8cf95747d0f9d5d72a7f5571 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Wed, 14 Dec 2022 17:57:01 +0100 Subject: [PATCH] Resetting filter doesn't reset sorting, some bugfixes and UI updates --- integration_test/login_integration_test.dart | 4 - integration_test/src/framework.dart | 3 +- .../impl/saved_view_repository_impl.dart | 14 +- .../repository/saved_view_repository.dart | 2 +- lib/di_initializer.dart | 5 +- .../view/pages/document_details_page.dart | 159 ++++++++---------- .../documents/bloc/documents_cubit.dart | 7 + .../documents/view/pages/documents_page.dart | 74 +++++++- .../view/widgets/grid/document_grid.dart | 6 + .../view/widgets/list/document_list.dart | 14 +- .../view/widgets/list/document_list_item.dart | 13 +- .../widgets/search/document_filter_panel.dart | 7 +- lib/features/edit_label/view/label_form.dart | 2 +- lib/features/home/view/home_page.dart | 1 - .../view/widgets/correspondent_widget.dart | 37 ++-- .../view/widgets/document_type_widget.dart | 36 +--- .../view/widgets/storage_path_widget.dart | 28 +-- .../login/bloc/authentication_cubit.dart | 25 ++- .../saved_view/cubit/saved_view_cubit.dart | 30 ++-- .../saved_view/cubit/saved_view_state.dart | 4 + .../view/saved_view_selection_widget.dart | 77 ++++++--- lib/main.dart | 30 +++- .../labels_api/paperless_labels_api_impl.dart | 2 +- 23 files changed, 327 insertions(+), 253 deletions(-) diff --git a/integration_test/login_integration_test.dart b/integration_test/login_integration_test.dart index e5fdfc7..e68514e 100644 --- a/integration_test/login_integration_test.dart +++ b/integration_test/login_integration_test.dart @@ -49,7 +49,6 @@ void main() async { await getIt().initialize(); await getIt().initialize(); - await getIt().initialize(); }); // Mocked classes @@ -97,7 +96,6 @@ void main() async { await getIt().initialize(); await getIt().initialize(); - await getIt().initialize(); }); // Mocked classes @@ -149,7 +147,6 @@ void main() async { )); await getIt().initialize(); await getIt().initialize(); - await getIt().initialize(); }); await t.binding.waitUntilFirstFrameRasterized; @@ -199,7 +196,6 @@ void main() async { await getIt().initialize(); await getIt().initialize(); - await getIt().initialize(); }); await t.binding.waitUntilFirstFrameRasterized; diff --git a/integration_test/src/framework.dart b/integration_test/src/framework.dart index 1ba69d6..59c9368 100644 --- a/integration_test/src/framework.dart +++ b/integration_test/src/framework.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:paperless_mobile/di_initializer.dart'; +import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/main.dart'; @@ -35,5 +36,5 @@ Future initAndLaunchTestApp( Future Function() initializationCallback, ) async { await initializationCallback(); - runApp(const PaperlessMobileEntrypoint()); + //runApp(const PaperlessMobileEntrypoint(authenticationCubit: ),)); } diff --git a/lib/core/repository/impl/saved_view_repository_impl.dart b/lib/core/repository/impl/saved_view_repository_impl.dart index 6ccbfd1..dfeae1e 100644 --- a/lib/core/repository/impl/saved_view_repository_impl.dart +++ b/lib/core/repository/impl/saved_view_repository_impl.dart @@ -1,5 +1,4 @@ import 'package:paperless_api/paperless_api.dart'; -import 'package:paperless_api/src/models/saved_view_model.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:rxdart/rxdart.dart'; @@ -8,11 +7,10 @@ class SavedViewRepositoryImpl implements SavedViewRepository { SavedViewRepositoryImpl(this._api); - final BehaviorSubject> _subject = - BehaviorSubject.seeded({}); + final BehaviorSubject?> _subject = BehaviorSubject(); @override - Stream> get savedViews => + Stream?> get savedViews => _subject.stream.asBroadcastStream(); @override @@ -23,7 +21,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository { @override Future create(SavedView view) async { final created = await _api.save(view); - final updatedState = {..._subject.value} + final updatedState = {..._subject.valueOrNull ?? {}} ..putIfAbsent(created.id!, () => created); _subject.add(updatedState); return created; @@ -32,7 +30,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository { @override Future delete(SavedView view) async { await _api.delete(view); - final updatedState = {..._subject.value}..remove(view.id); + final updatedState = {..._subject.valueOrNull ?? {}}..remove(view.id); _subject.add(updatedState); return view.id!; } @@ -40,7 +38,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository { @override Future find(int id) async { final found = await _api.find(id); - final updatedState = {..._subject.value} + final updatedState = {..._subject.valueOrNull ?? {}} ..update(id, (_) => found, ifAbsent: () => found); _subject.add(updatedState); return found; @@ -50,7 +48,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository { Future> findAll([Iterable? ids]) async { final found = await _api.findAll(ids); final updatedState = { - ..._subject.value, + ..._subject.valueOrNull ?? {}, ...{for (final view in found) view.id!: view}, }; _subject.add(updatedState); diff --git a/lib/core/repository/saved_view_repository.dart b/lib/core/repository/saved_view_repository.dart index cb57caf..50dce85 100644 --- a/lib/core/repository/saved_view_repository.dart +++ b/lib/core/repository/saved_view_repository.dart @@ -1,7 +1,7 @@ import 'package:paperless_api/paperless_api.dart'; abstract class SavedViewRepository { - Stream> get savedViews; + Stream?> get savedViews; Future create(SavedView view); Future find(int id); diff --git a/lib/di_initializer.dart b/lib/di_initializer.dart index 3351135..08bcb22 100644 --- a/lib/di_initializer.dart +++ b/lib/di_initializer.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:paperless_mobile/di_initializer.config.dart'; import 'package:paperless_mobile/di_modules.dart'; -import 'package:paperless_mobile/di_paperless_api.dart'; import 'package:paperless_mobile/features/login/model/client_certificate.dart'; import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; @@ -21,7 +20,7 @@ void configureDependencies(String environment) => /// /// Registers new security context, which will be used by the HttpClient, see [RegisterModule]. /// -void registerSecurityContext(ClientCertificate? cert) { +Future registerSecurityContext(ClientCertificate? cert) async { var context = SecurityContext(); if (cert != null) { context = context @@ -29,6 +28,6 @@ void registerSecurityContext(ClientCertificate? cert) { ..useCertificateChainBytes(cert.bytes, password: cert.passphrase) ..setTrustedCertificatesBytes(cert.bytes, password: cert.passphrase); } - getIt.unregister(); + await getIt.unregister(); getIt.registerSingleton(context); } 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 1329613..15ba01b 100644 --- a/lib/features/document_details/view/pages/document_details_page.dart +++ b/lib/features/document_details/view/pages/document_details_page.dart @@ -107,11 +107,11 @@ class _DocumentDetailsPageState extends State { color: Colors .black, //TODO: check if there is a way to dynamically determine color... ), - onPressed: () => Navigator.pop( - context, - BlocProvider.of(context) - .state - .document), + onPressed: () => Navigator.of(context).pop( + BlocProvider.of(context) + .state + .document, + ), ), floating: true, pinned: true, @@ -237,13 +237,13 @@ class _DocumentDetailsPageState extends State { return ListView( children: [ _DetailsItem.text(DateFormat().format(document.modified), - label: S.of(context).documentModifiedPropertyLabel, - context: context), - _separator(), + label: S.of(context).documentModifiedPropertyLabel, + context: context) + .paddedOnly(bottom: 16), _DetailsItem.text(DateFormat().format(document.added), - label: S.of(context).documentAddedPropertyLabel, - context: context), - _separator(), + label: S.of(context).documentAddedPropertyLabel, + context: context) + .paddedSymmetrically(vertical: 16), _DetailsItem( label: S.of(context).documentArchiveSerialNumberPropertyLongLabel, content: document.archiveSerialNumber != null @@ -255,30 +255,26 @@ class _DocumentDetailsPageState extends State { onPressed: widget.allowEdit ? () => _assignAsn(document) : null, ), - ), - _separator(), + ).paddedSymmetrically(vertical: 16), _DetailsItem.text( meta.mediaFilename, context: context, label: S.of(context).documentMetaDataMediaFilenamePropertyLabel, - ), - _separator(), + ).paddedSymmetrically(vertical: 16), _DetailsItem.text( meta.originalChecksum, context: context, label: S.of(context).documentMetaDataChecksumLabel, - ), - _separator(), + ).paddedSymmetrically(vertical: 16), _DetailsItem.text(formatBytes(meta.originalSize, 2), - label: S.of(context).documentMetaDataOriginalFileSizeLabel, - context: context), - _separator(), + label: S.of(context).documentMetaDataOriginalFileSizeLabel, + context: context) + .paddedSymmetrically(vertical: 16), _DetailsItem.text( meta.originalMimeType, label: S.of(context).documentMetaDataOriginalMimeTypeLabel, context: context, - ), - _separator(), + ).paddedSymmetrically(vertical: 16), ], ); }, @@ -295,16 +291,13 @@ class _DocumentDetailsPageState extends State { Widget _buildDocumentContentView(DocumentModel document, String? match) { return SingleChildScrollView( - child: _DetailsItem( - content: HighlightedText( - text: document.content ?? "", - highlights: match == null ? [] : match.split(" "), - style: Theme.of(context).textTheme.bodyText2, - caseSensitive: false, - ), - label: S.of(context).documentDetailsPageTabContentLabel, + child: HighlightedText( + text: document.content ?? "", + highlights: match == null ? [] : match.split(" "), + style: Theme.of(context).textTheme.bodyText2, + caseSensitive: false, ), - ); + ).paddedOnly(top: 8); } Widget _buildDocumentOverview(DocumentModel document, String? match) { @@ -314,60 +307,61 @@ class _DocumentDetailsPageState extends State { content: HighlightedText( text: document.title, highlights: match?.split(" ") ?? [], + style: Theme.of(context).textTheme.bodyLarge, ), label: S.of(context).documentTitlePropertyLabel, - ), - _separator(), + ).paddedOnly(bottom: 16), _DetailsItem.text( DateFormat.yMMMd().format(document.created), context: context, label: S.of(context).documentCreatedPropertyLabel, - ), - _separator(), - _DetailsItem( - content: DocumentTypeWidget( - isClickable: widget.isLabelClickable, - documentTypeId: document.documentType, - afterSelected: () { - Navigator.pop(context); - }, - ), - label: S.of(context).documentDocumentTypePropertyLabel, - ), - _separator(), - _DetailsItem( - label: S.of(context).documentCorrespondentPropertyLabel, - content: CorrespondentWidget( - isClickable: widget.isLabelClickable, - correspondentId: document.correspondent, - afterSelected: () { - Navigator.pop(context); - }, - ), - ), - _separator(), - _DetailsItem( - label: S.of(context).documentStoragePathPropertyLabel, - content: StoragePathWidget( - isClickable: widget.isLabelClickable, - pathId: document.storagePath, - afterSelected: () { - Navigator.pop(context); - }, - ), - ), - _separator(), - _DetailsItem( - label: S.of(context).documentTagsPropertyLabel, - content: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: TagsWidget( + ).paddedSymmetrically(vertical: 16), + Visibility( + visible: document.documentType != null, + child: _DetailsItem( + content: DocumentTypeWidget( + textStyle: Theme.of(context).textTheme.bodyLarge, isClickable: widget.isLabelClickable, - tagIds: document.tags, - isSelectedPredicate: (_) => false, - onTagSelected: (int tagId) {}, + documentTypeId: document.documentType, ), - ), + label: S.of(context).documentDocumentTypePropertyLabel, + ).paddedSymmetrically(vertical: 16), + ), + Visibility( + visible: document.correspondent != null, + child: _DetailsItem( + label: S.of(context).documentCorrespondentPropertyLabel, + content: CorrespondentWidget( + textStyle: Theme.of(context).textTheme.bodyLarge, + isClickable: widget.isLabelClickable, + correspondentId: document.correspondent, + ), + ).paddedSymmetrically(vertical: 16), + ), + Visibility( + visible: document.storagePath != null, + child: _DetailsItem( + label: S.of(context).documentStoragePathPropertyLabel, + content: StoragePathWidget( + isClickable: widget.isLabelClickable, + pathId: document.storagePath, + ), + ).paddedSymmetrically(vertical: 16), + ), + Visibility( + visible: document.tags.isNotEmpty, + child: _DetailsItem( + label: S.of(context).documentTagsPropertyLabel, + content: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TagsWidget( + isClickable: widget.isLabelClickable, + tagIds: document.tags, + isSelectedPredicate: (_) => false, + onTagSelected: (int tagId) {}, + ), + ), + ).paddedSymmetrically(vertical: 16), ), // _separator(), // FutureBuilder>( @@ -396,10 +390,6 @@ class _DocumentDetailsPageState extends State { ); } - Widget _separator() { - return const SizedBox(height: 32.0); - } - /// /// Downloads file to temporary directory, from which it can then be shared. /// @@ -477,10 +467,7 @@ class _DetailsItem extends StatelessWidget { children: [ Text( label, - style: Theme.of(context) - .textTheme - .headline5 - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.caption, ), content, ], @@ -492,7 +479,7 @@ class _DetailsItem extends StatelessWidget { String text, { required this.label, required BuildContext context, - }) : content = Text(text, style: Theme.of(context).textTheme.bodyText2); + }) : content = Text(text, style: Theme.of(context).textTheme.bodyLarge); } class ColoredTabBar extends Container implements PreferredSizeWidget { diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index abd1873..1d92526 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -88,6 +88,13 @@ class DocumentsCubit extends Cubit { emit(DocumentsState(filter: filter, value: [result], isLoaded: true)); } + Future resetFilter() async { + final filter = DocumentFilter.initial.copyWith( + sortField: state.filter.sortField, + sortOrder: state.filter.sortOrder, + ); + } + /// /// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter. /// diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 4450591..d6b1915 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -151,22 +151,28 @@ class _DocumentsPageState extends State { switch (settings.preferredViewType) { case ViewType.list: child = DocumentListView( - onTap: _openDetails, state: state, + onTap: _openDetails, onSelected: _onSelected, pagingController: _pagingController, hasInternetConnection: isConnected, onTagSelected: _addTagToFilter, + onCorrespondentSelected: _addCorrespondentToFilter, + onDocumentTypeSelected: _addDocumentTypeToFilter, + onStoragePathSelected: _addStoragePathToFilter, ); break; case ViewType.grid: child = DocumentGridView( - onTap: _openDetails, state: state, + onTap: _openDetails, onSelected: _onSelected, pagingController: _pagingController, hasInternetConnection: isConnected, onTagSelected: _addTagToFilter, + onCorrespondentSelected: _addCorrespondentToFilter, + onDocumentTypeSelected: _addDocumentTypeToFilter, + onStoragePathSelected: _addStoragePathToFilter, ); break; } @@ -176,7 +182,7 @@ class _DocumentsPageState extends State { child: DocumentsEmptyState( state: state, onReset: () { - _documentsCubit.updateFilter(); + _documentsCubit.resetFilter(); _savedViewCubit.resetSelection(); }, ), @@ -195,7 +201,7 @@ class _DocumentsPageState extends State { listener: (context, state) { try { if (state.selectedSavedViewId == null) { - _documentsCubit.updateFilter(); + _documentsCubit.resetFilter(); } else { final newFilter = state .value[state.selectedSavedViewId] @@ -280,6 +286,63 @@ class _DocumentsPageState extends State { } } + void _addCorrespondentToFilter(int? correspondentId) { + final cubit = BlocProvider.of(context); + try { + if (cubit.state.filter.correspondent.id == correspondentId) { + cubit.updateCurrentFilter( + (filter) => + filter.copyWith(correspondent: const CorrespondentQuery.unset()), + ); + } else { + cubit.updateCurrentFilter( + (filter) => filter.copyWith( + correspondent: CorrespondentQuery.fromId(correspondentId)), + ); + } + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + void _addDocumentTypeToFilter(int? documentTypeId) { + final cubit = BlocProvider.of(context); + try { + if (cubit.state.filter.documentType.id == documentTypeId) { + cubit.updateCurrentFilter( + (filter) => + filter.copyWith(documentType: const DocumentTypeQuery.unset()), + ); + } else { + cubit.updateCurrentFilter( + (filter) => filter.copyWith( + documentType: DocumentTypeQuery.fromId(documentTypeId)), + ); + } + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + + void _addStoragePathToFilter(int? pathId) { + final cubit = BlocProvider.of(context); + try { + if (cubit.state.filter.correspondent.id == pathId) { + cubit.updateCurrentFilter( + (filter) => + filter.copyWith(storagePath: const StoragePathQuery.unset()), + ); + } else { + cubit.updateCurrentFilter( + (filter) => + filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)), + ); + } + } on PaperlessServerException catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } + Future _loadNewPage(int pageKey) async { final pageCount = _documentsCubit.state .inferPageCount(pageSize: _documentsCubit.state.filter.pageSize); @@ -299,9 +362,10 @@ class _DocumentsPageState extends State { Future _onRefresh() async { try { - await _documentsCubit.updateCurrentFilter( + _documentsCubit.updateCurrentFilter( (filter) => filter.copyWith(page: 1), ); + _savedViewCubit.reload(); } on PaperlessServerException catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } diff --git a/lib/features/documents/view/widgets/grid/document_grid.dart b/lib/features/documents/view/widgets/grid/document_grid.dart index 16a532d..15f73c1 100644 --- a/lib/features/documents/view/widgets/grid/document_grid.dart +++ b/lib/features/documents/view/widgets/grid/document_grid.dart @@ -12,6 +12,9 @@ class DocumentGridView extends StatelessWidget { final DocumentsState state; final bool hasInternetConnection; final void Function(int tagId) onTagSelected; + final void Function(int correspondentId) onCorrespondentSelected; + final void Function(int correspondentId) onDocumentTypeSelected; + final void Function(int? id)? onStoragePathSelected; const DocumentGridView({ super.key, @@ -21,6 +24,9 @@ class DocumentGridView extends StatelessWidget { required this.onSelected, required this.hasInternetConnection, required this.onTagSelected, + required this.onCorrespondentSelected, + required this.onDocumentTypeSelected, + this.onStoragePathSelected, }); @override Widget build(BuildContext context) { diff --git a/lib/features/documents/view/widgets/list/document_list.dart b/lib/features/documents/view/widgets/list/document_list.dart index 2dcae35..8fac591 100644 --- a/lib/features/documents/view/widgets/list/document_list.dart +++ b/lib/features/documents/view/widgets/list/document_list.dart @@ -1,5 +1,4 @@ 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/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; @@ -16,7 +15,10 @@ class DocumentListView extends StatelessWidget { final DocumentsState state; final bool hasInternetConnection; final bool isLabelClickable; - final void Function(int tagId) onTagSelected; + final void Function(int id)? onTagSelected; + final void Function(int? id)? onCorrespondentSelected; + final void Function(int? id)? onDocumentTypeSelected; + final void Function(int? id)? onStoragePathSelected; const DocumentListView({ super.key, @@ -26,7 +28,10 @@ class DocumentListView extends StatelessWidget { required this.onSelected, required this.hasInternetConnection, this.isLabelClickable = true, - required this.onTagSelected, + this.onTagSelected, + this.onCorrespondentSelected, + this.onDocumentTypeSelected, + this.onStoragePathSelected, }); @override @@ -52,6 +57,9 @@ class DocumentListView extends StatelessWidget { : false; }, onTagSelected: onTagSelected, + onCorrespondentSelected: onCorrespondentSelected, + onDocumentTypeSelected: onDocumentTypeSelected, + onStoragePathSelected: onStoragePathSelected, ), ); }, diff --git a/lib/features/documents/view/widgets/list/document_list_item.dart b/lib/features/documents/view/widgets/list/document_list_item.dart index ec5e4d4..e73c97b 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -14,7 +14,10 @@ class DocumentListItem extends StatelessWidget { final bool isLabelClickable; final bool Function(int tagId) isTagSelectedPredicate; - final void Function(int tagId) onTagSelected; + final void Function(int tagId)? onTagSelected; + final void Function(int? correspondentId)? onCorrespondentSelected; + final void Function(int? documentTypeId)? onDocumentTypeSelected; + final void Function(int? id)? onStoragePathSelected; const DocumentListItem({ Key? key, @@ -25,7 +28,10 @@ class DocumentListItem extends StatelessWidget { required this.isAtLeastOneSelected, this.isLabelClickable = true, required this.isTagSelectedPredicate, - required this.onTagSelected, + this.onTagSelected, + this.onCorrespondentSelected, + this.onDocumentTypeSelected, + this.onStoragePathSelected, }) : super(key: key); @override @@ -48,6 +54,7 @@ class DocumentListItem extends StatelessWidget { child: CorrespondentWidget( isClickable: isLabelClickable, correspondentId: document.correspondent, + onSelected: onCorrespondentSelected, ), ), ], @@ -68,7 +75,7 @@ class DocumentListItem extends StatelessWidget { tagIds: document.tags, isMultiLine: false, isSelectedPredicate: isTagSelectedPredicate, - onTagSelected: onTagSelected, + onTagSelected: (id) => onTagSelected?.call(id), ), ), ), 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 65d06b0..5549913 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -123,7 +123,12 @@ class _DocumentFilterPanelState extends State { void _resetFilter() async { FocusScope.of(context).unfocus(); - Navigator.pop(context, DocumentFilter.initial); + Navigator.pop( + context, + DocumentFilter.initial.copyWith( + sortField: widget.initialFilter.sortField, + sortOrder: widget.initialFilter.sortOrder, + )); } Widget _buildDocumentTypeFormField() { diff --git a/lib/features/edit_label/view/label_form.dart b/lib/features/edit_label/view/label_form.dart index 0b319ff..7938932 100644 --- a/lib/features/edit_label/view/label_form.dart +++ b/lib/features/edit_label/view/label_form.dart @@ -100,7 +100,7 @@ class _LabelFormState extends State> { ), FormBuilderCheckbox( name: Label.isInsensitiveKey, - initialValue: widget.initialValue?.isInsensitive, + initialValue: widget.initialValue?.isInsensitive ?? true, title: Text(S.of(context).labelIsInsensivitePropertyLabel), ), ...widget.additionalFields, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index 453ff2e..207a01f 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; -import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 296a0ec..762502f 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -9,16 +9,18 @@ import 'package:paperless_mobile/util.dart'; class CorrespondentWidget extends StatelessWidget { final int? correspondentId; - final void Function()? afterSelected; + final void Function(int? id)? onSelected; final Color? textColor; final bool isClickable; + final TextStyle? textStyle; const CorrespondentWidget({ Key? key, - this.correspondentId, - this.afterSelected, + required this.correspondentId, this.textColor, this.isClickable = true, + this.textStyle, + this.onSelected, }) : super(key: key); @override @@ -30,14 +32,15 @@ class CorrespondentWidget extends StatelessWidget { BlocBuilder, LabelState>( builder: (context, state) { return GestureDetector( - onTap: () => _addCorrespondentToFilter(context), + onTap: () => onSelected?.call(correspondentId!), child: Text( (state.getLabel(correspondentId)?.name) ?? "-", maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2?.copyWith( - color: textColor ?? Theme.of(context).colorScheme.primary, - ), + style: (textStyle ?? Theme.of(context).textTheme.bodyText2) + ?.copyWith( + color: textColor ?? Theme.of(context).colorScheme.primary, + ), ), ); }, @@ -45,24 +48,4 @@ class CorrespondentWidget extends StatelessWidget { ), ); } - - void _addCorrespondentToFilter(BuildContext context) { - final cubit = BlocProvider.of(context); - try { - if (cubit.state.filter.correspondent.id == correspondentId) { - cubit.updateCurrentFilter( - (filter) => - filter.copyWith(correspondent: const CorrespondentQuery.unset()), - ); - } else { - cubit.updateCurrentFilter( - (filter) => filter.copyWith( - correspondent: CorrespondentQuery.fromId(correspondentId)), - ); - } - afterSelected?.call(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/features/labels/document_type/view/widgets/document_type_widget.dart b/lib/features/labels/document_type/view/widgets/document_type_widget.dart index b0108ab..24b7792 100644 --- a/lib/features/labels/document_type/view/widgets/document_type_widget.dart +++ b/lib/features/labels/document_type/view/widgets/document_type_widget.dart @@ -2,20 +2,20 @@ 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/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/util.dart'; class DocumentTypeWidget extends StatelessWidget { final int? documentTypeId; - final void Function()? afterSelected; final bool isClickable; + final TextStyle? textStyle; + final void Function(int? id)? onSelected; const DocumentTypeWidget({ Key? key, required this.documentTypeId, - this.afterSelected, this.isClickable = true, + this.textStyle, + this.onSelected, }) : super(key: key); @override @@ -27,16 +27,14 @@ class DocumentTypeWidget extends StatelessWidget { child: AbsorbPointer( absorbing: !isClickable, child: GestureDetector( - onTap: () => _addDocumentTypeToFilter(context), + onTap: () => onSelected?.call(documentTypeId), child: BlocBuilder, LabelState>( builder: (context, state) { return Text( state.labels[documentTypeId]?.toString() ?? "-", - style: Theme.of(context) - .textTheme - .bodyText2! - .copyWith(color: Theme.of(context).colorScheme.tertiary), + style: (textStyle ?? Theme.of(context).textTheme.bodyText2) + ?.copyWith(color: Theme.of(context).colorScheme.tertiary), ); }, ), @@ -44,24 +42,4 @@ class DocumentTypeWidget extends StatelessWidget { ), ); } - - void _addDocumentTypeToFilter(BuildContext context) { - final cubit = BlocProvider.of(context); - try { - if (cubit.state.filter.documentType.id == documentTypeId) { - cubit.updateCurrentFilter( - (filter) => - filter.copyWith(documentType: const DocumentTypeQuery.unset()), - ); - } else { - cubit.updateCurrentFilter( - (filter) => filter.copyWith( - documentType: DocumentTypeQuery.fromId(documentTypeId)), - ); - } - afterSelected?.call(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart index ce3fd07..f7f735d 100644 --- a/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart +++ b/lib/features/labels/storage_path/view/widgets/storage_path_widget.dart @@ -2,23 +2,21 @@ 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/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; -import 'package:paperless_mobile/util.dart'; class StoragePathWidget extends StatelessWidget { final int? pathId; - final void Function()? afterSelected; final Color? textColor; final bool isClickable; + final void Function(int? id)? onSelected; const StoragePathWidget({ Key? key, this.pathId, - this.afterSelected, this.textColor, this.isClickable = true, + this.onSelected, }) : super(key: key); @override @@ -32,7 +30,7 @@ class StoragePathWidget extends StatelessWidget { child: BlocBuilder, LabelState>( builder: (context, state) { return GestureDetector( - onTap: () => _addStoragePathToFilter(context), + onTap: () => onSelected?.call(pathId), child: Text( state.getLabel(pathId)?.name ?? "-", maxLines: 1, @@ -47,24 +45,4 @@ class StoragePathWidget extends StatelessWidget { ), ); } - - void _addStoragePathToFilter(BuildContext context) { - final cubit = BlocProvider.of(context); - try { - if (cubit.state.filter.correspondent.id == pathId) { - cubit.updateCurrentFilter( - (filter) => - filter.copyWith(storagePath: const StoragePathQuery.unset()), - ); - } else { - cubit.updateCurrentFilter( - (filter) => - filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)), - ); - } - afterSelected?.call(); - } on PaperlessServerException catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - } } diff --git a/lib/features/login/bloc/authentication_cubit.dart b/lib/features/login/bloc/authentication_cubit.dart index e63d6a5..d360a65 100644 --- a/lib/features/login/bloc/authentication_cubit.dart +++ b/lib/features/login/bloc/authentication_cubit.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -11,12 +10,9 @@ import 'package:paperless_mobile/features/login/model/user_credentials.model.dar import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; -@prod -@test -@singleton class AuthenticationCubit extends Cubit { final LocalAuthenticationService _localAuthService; - final PaperlessAuthenticationApi _authApi; + PaperlessAuthenticationApi _authApi; final LocalVault _localVault; AuthenticationCubit( @@ -25,10 +21,6 @@ class AuthenticationCubit extends Cubit { this._authApi, ) : super(AuthenticationState.initial); - Future initialize() { - return restoreSessionState(); - } - Future login({ required UserCredentials credentials, required String serverUrl, @@ -37,6 +29,8 @@ class AuthenticationCubit extends Cubit { assert(credentials.username != null && credentials.password != null); try { registerSecurityContext(clientCertificate); + //TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive. + _authApi = getIt(); // Store information required to make requests final currentAuth = AuthenticationInformation( serverUrl: serverUrl, @@ -82,13 +76,16 @@ class AuthenticationCubit extends Cubit { } if (storedAuth == null || !storedAuth.isValid) { return emit( - AuthenticationState(isAuthenticated: false, wasLoginStored: false)); + AuthenticationState(isAuthenticated: false, wasLoginStored: false), + ); } else { if (appSettings.isLocalAuthenticationEnabled) { final localAuthSuccess = await _localAuthService .authenticateLocalUser("Authenticate to log back in"); if (localAuthSuccess) { - registerSecurityContext(storedAuth.clientCertificate); + await registerSecurityContext(storedAuth.clientCertificate); + //TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive. + _authApi = getIt(); return emit( AuthenticationState( isAuthenticated: true, @@ -105,11 +102,13 @@ class AuthenticationCubit extends Cubit { )); } } else { - return emit(AuthenticationState( + await registerSecurityContext(storedAuth.clientCertificate); + final authState = AuthenticationState( isAuthenticated: true, authentication: storedAuth, wasLoginStored: true, - )); + ); + return emit(authState); } } } diff --git a/lib/features/saved_view/cubit/saved_view_cubit.dart b/lib/features/saved_view/cubit/saved_view_cubit.dart index 3df3e75..1373f28 100644 --- a/lib/features/saved_view/cubit/saved_view_cubit.dart +++ b/lib/features/saved_view/cubit/saved_view_cubit.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:injectable/injectable.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; @@ -12,22 +11,26 @@ class SavedViewCubit extends Cubit { SavedViewCubit(this._repository) : super(SavedViewState(value: {})) { _subscription = _repository.savedViews.listen( - (savedViews) => emit(state.copyWith(value: savedViews)), + (savedViews) { + if (savedViews == null) { + emit(state.copyWith(isLoaded: false)); + } else { + emit(state.copyWith(value: savedViews, isLoaded: true)); + } + }, ); } void selectView(SavedView? view) { - emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id)); + emit(state.copyWith( + selectedSavedViewId: view?.id, + overwriteSelectedSavedViewId: true, + )); } Future add(SavedView view) async { final savedView = await _repository.create(view); - emit( - SavedViewState( - value: {...state.value, savedView.id!: savedView}, - selectedSavedViewId: state.selectedSavedViewId, - ), - ); + emit(state.copyWith(value: {...state.value, savedView.id!: savedView})); return savedView; } @@ -42,11 +45,16 @@ class SavedViewCubit extends Cubit { Future initialize() async { final views = await _repository.findAll(); final values = {for (var element in views) element.id!: element}; - emit(SavedViewState(value: values)); + emit(SavedViewState(value: values, isLoaded: true)); } + Future reload() => initialize(); + void resetSelection() { - emit(SavedViewState(value: state.value)); + emit(SavedViewState( + value: state.value, + isLoaded: true, + )); } @override diff --git a/lib/features/saved_view/cubit/saved_view_state.dart b/lib/features/saved_view/cubit/saved_view_state.dart index 682eb88..a83a65d 100644 --- a/lib/features/saved_view/cubit/saved_view_state.dart +++ b/lib/features/saved_view/cubit/saved_view_state.dart @@ -2,11 +2,13 @@ import 'package:equatable/equatable.dart'; import 'package:paperless_api/paperless_api.dart'; class SavedViewState with EquatableMixin { + final bool isLoaded; final Map value; final int? selectedSavedViewId; SavedViewState({ required this.value, + this.isLoaded = false, this.selectedSavedViewId, }); @@ -20,9 +22,11 @@ class SavedViewState with EquatableMixin { Map? value, int? selectedSavedViewId, bool overwriteSelectedSavedViewId = false, + bool? isLoaded, }) { return SavedViewState( value: value ?? this.value, + isLoaded: isLoaded ?? this.isLoaded, selectedSavedViewId: overwriteSelectedSavedViewId ? selectedSavedViewId : this.selectedSavedViewId, 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 6ce9e58..01ffa92 100644 --- a/lib/features/saved_view/view/saved_view_selection_widget.dart +++ b/lib/features/saved_view/view/saved_view_selection_widget.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; @@ -8,6 +10,7 @@ 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'; +import 'package:shimmer/shimmer.dart'; class SavedViewSelectionWidget extends StatelessWidget { final DocumentFilter currentFilter; @@ -29,6 +32,9 @@ class SavedViewSelectionWidget extends StatelessWidget { children: [ BlocBuilder( builder: (context, state) { + if (!state.isLoaded) { + return _buildLoadingWidget(context); + } if (state.value.isEmpty) { return Text(S.of(context).savedViewsEmptyStateText); } @@ -58,32 +64,61 @@ class SavedViewSelectionWidget extends StatelessWidget { ); }, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).savedViewsLabel, - style: Theme.of(context).textTheme.titleSmall, - ), - 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), - ); - }, - ), - ], + BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).savedViewsLabel, + style: Theme.of(context).textTheme.titleSmall, + ), + BlocBuilder( + buildWhen: (previous, current) => + previous.filter != current.filter, + builder: (context, docState) { + return TextButton.icon( + icon: const Icon(Icons.add), + onPressed: (enabled && state.isLoaded) + ? () => _onCreatePressed(context, docState.filter) + : null, + label: Text(S.of(context).savedViewCreateNewLabel), + ); + }, + ), + ], + ); + }, ), ], ); } + Widget _buildLoadingWidget(BuildContext context) { + final r = Random(123456789); + return SizedBox( + height: height, + width: double.infinity, + child: Shimmer.fromColors( + baseColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[300]! + : Colors.grey[900]!, + highlightColor: Theme.of(context).brightness == Brightness.light + ? Colors.grey[100]! + : Colors.grey[600]!, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + itemBuilder: (context, index) => FilterChip( + label: SizedBox(width: r.nextInt((index * 20) + 50).toDouble()), + onSelected: null), + separatorBuilder: (context, index) => SizedBox(width: 8.0), + ), + ), + ); + } + void _onCreatePressed(BuildContext context, DocumentFilter filter) async { final newView = await Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/main.dart b/lib/main.dart index 94e37c7..b33cde3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -34,6 +35,7 @@ import 'package:paperless_mobile/features/document_upload/cubit/document_upload_ import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart'; import 'package:paperless_mobile/features/home/view/home_page.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; +import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/view/login_page.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; @@ -57,7 +59,13 @@ void main() async { // Load application settings and stored authentication data await getIt().initialize(); await getIt().initialize(); - await getIt().initialize(); + + final authCubit = AuthenticationCubit( + getIt(), + getIt(), + getIt(), + ); + await authCubit.restoreSessionState(); // Create repositories final LabelRepository tagRepository = @@ -80,13 +88,17 @@ void main() async { RepositoryProvider.value(value: storagePathRepository), RepositoryProvider.value(value: savedViewRepository), ], - child: const PaperlessMobileEntrypoint(), + child: PaperlessMobileEntrypoint(authenticationCubit: authCubit), ), ); } class PaperlessMobileEntrypoint extends StatefulWidget { - const PaperlessMobileEntrypoint({Key? key}) : super(key: key); + final AuthenticationCubit authenticationCubit; + const PaperlessMobileEntrypoint({ + Key? key, + required this.authenticationCubit, + }) : super(key: key); @override State createState() => @@ -165,8 +177,8 @@ class _PaperlessMobileEntrypointState extends State { GlobalWidgetsLocalizations.delegate, FormBuilderLocalizations.delegate, ], - home: BlocProvider.value( - value: getIt(), + home: BlocProvider.value( + value: widget.authenticationCubit, child: const AuthenticationWrapper(), ), ); @@ -291,10 +303,10 @@ class _AuthenticationWrapperState extends State { if (authentication.isAuthenticated) { return const HomePage(); } else { - // if (authentication.wasLoginStored && - // !(authentication.wasLocalAuthenticationSuccessful ?? false)) { - // return const BiometricAuthenticationPage(); - // } + if (authentication.wasLoginStored && + !(authentication.wasLocalAuthenticationSuccessful ?? false)) { + return const BiometricAuthenticationPage(); + } return const LoginPage(); } }, diff --git a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart index 6c4b60b..7ab8548 100644 --- a/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart +++ b/packages/paperless_api/lib/src/modules/labels_api/paperless_labels_api_impl.dart @@ -261,7 +261,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi { @override Future getStoragePath(int id) { return getSingleResult( - "/api/storage_paths/?page=1&page_size=100000", + "/api/storage_paths/$id/", StoragePath.fromJson, ErrorCode.storagePathLoadFailed, client: client,