diff --git a/lib/core/bloc/label_cubit.dart b/lib/core/bloc/label_cubit.dart deleted file mode 100644 index 07c3580..0000000 --- a/lib/core/bloc/label_cubit.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/features/labels/model/label.model.dart'; -import 'package:paperless_mobile/features/labels/repository/label_repository.dart'; - -abstract class LabelCubit extends Cubit> { - final LabelRepository labelRepository; - - LabelCubit(this.labelRepository) : super({}); - - @protected - void loadFrom(Iterable items) => - emit(Map.fromIterable(items, key: (e) => (e as T).id!)); - - Future add(T item) async { - assert(item.id == null); - final addedItem = await save(item); - final newState = {...state}; - newState.putIfAbsent(addedItem.id!, () => addedItem); - emit(newState); - return addedItem; - } - - Future replace(T item) async { - assert(item.id != null); - final updatedItem = await update(item); - final newState = {...state}; - newState[item.id!] = updatedItem; - emit(newState); - return updatedItem; - } - - Future remove(T item) async { - assert(item.id != null); - if (state.containsKey(item.id)) { - final deletedId = await delete(item); - final newState = {...state}; - newState.remove(deletedId); - emit(newState); - } - } - - void reset() => emit({}); - - Future initialize(); - - @protected - Future save(T item); - - @protected - Future update(T item); - - @protected - Future delete(T item); -} diff --git a/lib/core/bloc/paperless_server_information_cubit.dart b/lib/core/bloc/paperless_server_information_cubit.dart index 4764c43..5c53cac 100644 --- a/lib/core/bloc/paperless_server_information_cubit.dart +++ b/lib/core/bloc/paperless_server_information_cubit.dart @@ -11,7 +11,7 @@ class PaperlessServerInformationCubit PaperlessServerInformationCubit(this.service) : super(PaperlessServerInformation()); - Future updateStatus() async { + Future updateInformtion() async { emit(await service.getInformation()); } } diff --git a/lib/core/bloc/paperless_statistics_cubit.dart b/lib/core/bloc/paperless_statistics_cubit.dart new file mode 100644 index 0000000..e2d1f0e --- /dev/null +++ b/lib/core/bloc/paperless_statistics_cubit.dart @@ -0,0 +1,62 @@ +import 'dart:math'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; +import 'package:paperless_mobile/core/service/paperless_statistics_service.dart'; + +@singleton +class PaperlessStatisticsCubit extends Cubit { + final PaperlessStatisticsService statisticsService; + + PaperlessStatisticsCubit(this.statisticsService) + : super(PaperlessStatisticsState(isLoaded: false)); + + Future updateStatistics() async { + final stats = await statisticsService.getStatistics(); + emit(PaperlessStatisticsState(isLoaded: true, statistics: stats)); + } + + void decrementInboxCount() { + if (state.isLoaded) { + emit( + PaperlessStatisticsState( + isLoaded: true, + statistics: PaperlessStatistics( + documentsInInbox: max(0, state.statistics!.documentsInInbox - 1), + documentsTotal: state.statistics!.documentsTotal, + ), + ), + ); + } + } + + void incrementInboxCount() { + if (state.isLoaded) { + emit( + PaperlessStatisticsState( + isLoaded: true, + statistics: PaperlessStatistics( + documentsInInbox: state.statistics!.documentsInInbox + 1, + documentsTotal: state.statistics!.documentsTotal, + ), + ), + ); + } + } + + void resetInboxCount() { + if (state.isLoaded) { + emit( + PaperlessStatisticsState( + isLoaded: true, + statistics: PaperlessStatistics( + documentsInInbox: 0, + documentsTotal: state.statistics!.documentsTotal, + ), + ), + ); + } + } +} diff --git a/lib/core/logic/error_code_localization_mapper.dart b/lib/core/logic/error_code_localization_mapper.dart index 9ad6c99..b1d4f28 100644 --- a/lib/core/logic/error_code_localization_mapper.dart +++ b/lib/core/logic/error_code_localization_mapper.dart @@ -38,8 +38,8 @@ String translateError(BuildContext context, ErrorCode code) { return S.of(context).errorMessageScanRemoveFailed; case ErrorCode.invalidClientCertificateConfiguration: return S.of(context).errorMessageInvalidClientCertificateConfiguration; - case ErrorCode.documentBulkDeleteFailed: - return S.of(context).errorMessageBulkDeleteDocumentsFailed; + case ErrorCode.documentBulkActionFailed: + return S.of(context).errorMessageBulkActionFailed; case ErrorCode.biometricsNotSupported: return S.of(context).errorMessageBiotmetricsNotSupported; case ErrorCode.biometricAuthenticationFailed: diff --git a/lib/core/model/error_message.dart b/lib/core/model/error_message.dart index 03d977d..098bbb1 100644 --- a/lib/core/model/error_message.dart +++ b/lib/core/model/error_message.dart @@ -27,7 +27,7 @@ enum ErrorCode { documentUpdateFailed, documentLoadFailed, documentDeleteFailed, - documentBulkDeleteFailed, + documentBulkActionFailed, documentPreviewFailed, documentAsnQueryFailed, tagCreateFailed, diff --git a/lib/core/model/paperless_statistics_state.dart b/lib/core/model/paperless_statistics_state.dart new file mode 100644 index 0000000..3cf1910 --- /dev/null +++ b/lib/core/model/paperless_statistics_state.dart @@ -0,0 +1,11 @@ +import 'package:paperless_mobile/core/model/paperless_statistics.dart'; + +class PaperlessStatisticsState { + final bool isLoaded; + final PaperlessStatistics? statistics; + + PaperlessStatisticsState({ + required this.isLoaded, + this.statistics, + }); +} diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 006451d..d3c552d 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/model/bulk_edit.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/paged_search_result.dart'; @@ -36,9 +37,9 @@ class DocumentsCubit extends Cubit { createdAt: createdAt, ); - // documentRepository - // .waitForConsumptionFinished(fileName, title) - // .then((value) => onConsumptionFinished(value)); + documentRepository + .waitForConsumptionFinished(fileName, title) + .then((value) => onConsumptionFinished(value)); } Future removeDocument(DocumentModel document) async { @@ -47,8 +48,23 @@ class DocumentsCubit extends Cubit { } Future bulkRemoveDocuments(List documents) async { - await documentRepository.bulkDelete(documents); - return await reloadDocuments(); + await documentRepository.bulkAction( + BulkDeleteAction(documents.map((doc) => doc.id)), + ); + await reloadDocuments(); + } + + Future bulkEditTags( + List documents, { + Iterable addTags = const [], + Iterable removeTags = const [], + }) async { + await documentRepository.bulkAction(BulkModifyTagsAction( + documents.map((doc) => doc.id), + addTags: addTags, + removeTags: removeTags, + )); + await reloadDocuments(); } Future updateDocument(DocumentModel document) async { @@ -135,15 +151,20 @@ class DocumentsCubit extends Cubit { } } - Future removeInboxTags( + /// + /// Updates the given document with the inbox tags removed and returns the remoed inbox tags. + /// + Future> removeInboxTags( DocumentModel document, final Iterable inboxTags) async { - final updatedTags = document.tags.where((id) => !inboxTags.contains(id)); - return updateDocument( + final tagsToRemove = document.tags.toSet().intersection(inboxTags.toSet()); + final updatedTags = {...document.tags}..removeAll(tagsToRemove); + await updateDocument( document.copyWith( tags: updatedTags, overwriteTags: true, ), ); + return tagsToRemove; } void resetSelection() { diff --git a/lib/features/documents/model/bulk_edit.model.dart b/lib/features/documents/model/bulk_edit.model.dart index 454dfb1..cabebab 100644 --- a/lib/features/documents/model/bulk_edit.model.dart +++ b/lib/features/documents/model/bulk_edit.model.dart @@ -1,24 +1,50 @@ import 'package:paperless_mobile/core/type/types.dart'; -class BulkEditAction { - final List documents; - final BulkEditActionMethod _method; - final Map parameters; +abstract class BulkAction { + final Iterable documentIds; - BulkEditAction.delete(this.documents) - : _method = BulkEditActionMethod.delete, - parameters = {}; + BulkAction(this.documentIds); + JSON toJson(); +} + +class BulkDeleteAction extends BulkAction { + BulkDeleteAction(super.documents); + + @override JSON toJson() { return { - 'documents': documents, - 'method': _method.name, - 'parameters': parameters, + 'documents': documentIds.toList(), + 'method': 'delete', }; } } -enum BulkEditActionMethod { - delete, - edit; +class BulkModifyTagsAction extends BulkAction { + final Iterable removeTags; + final Iterable addTags; + + BulkModifyTagsAction( + super.documents, { + this.removeTags = const [], + this.addTags = const [], + }); + + BulkModifyTagsAction.addTags(super.documents, this.addTags) + : removeTags = const []; + + BulkModifyTagsAction.removeTags(super.documents, this.removeTags) + : addTags = const []; + + @override + JSON toJson() { + return { + 'documents': documentIds.toList(), + 'method': 'modify_tags', + 'parameters': { + 'add_tags': addTags.toList(), + 'remove_tags': removeTags.toList(), + } + }; + } } diff --git a/lib/features/documents/model/query_parameters/tags_query.dart b/lib/features/documents/model/query_parameters/tags_query.dart index c2334f0..0bb91eb 100644 --- a/lib/features/documents/model/query_parameters/tags_query.dart +++ b/lib/features/documents/model/query_parameters/tags_query.dart @@ -17,13 +17,19 @@ class OnlyNotAssignedTagsQuery extends TagsQuery { } class AnyAssignedTagsQuery extends TagsQuery { - const AnyAssignedTagsQuery(); + final Iterable tagIds; + const AnyAssignedTagsQuery({ + this.tagIds = const [], + }); @override List get props => []; @override String toQueryParameter() { - return '&is_tagged=1'; + if (tagIds.isEmpty) { + return '&is_tagged=1'; + } + return '&tags__id__in=${tagIds.join(',')}'; } } diff --git a/lib/features/documents/repository/document_repository.dart b/lib/features/documents/repository/document_repository.dart index e5c84aa..27f1356 100644 --- a/lib/features/documents/repository/document_repository.dart +++ b/lib/features/documents/repository/document_repository.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:paperless_mobile/features/documents/model/bulk_edit.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/document_meta_data.model.dart'; @@ -23,7 +24,7 @@ abstract class DocumentRepository { Future> findSimilar(int docId); Future delete(DocumentModel doc); Future getMetaData(DocumentModel document); - Future> bulkDelete(List models); + Future> bulkAction(BulkAction action); Future getPreview(int docId); String getThumbnailUrl(int docId); Future waitForConsumptionFinished( diff --git a/lib/features/documents/repository/document_repository_impl.dart b/lib/features/documents/repository/document_repository_impl.dart index 2c07d18..49639f5 100644 --- a/lib/features/documents/repository/document_repository_impl.dart +++ b/lib/features/documents/repository/document_repository_impl.dart @@ -216,18 +216,16 @@ class DocumentRepositoryImpl implements DocumentRepository { } @override - Future> bulkDelete(List documentModels) async { - final List ids = documentModels.map((e) => e.id).toList(); - final action = BulkEditAction.delete(ids); + Future> bulkAction(BulkAction action) async { final response = await httpClient.post( Uri.parse("/api/documents/bulk_edit/"), body: json.encode(action.toJson()), headers: {'Content-Type': 'application/json'}, ); if (response.statusCode == 200) { - return ids; + return action.documentIds; } else { - throw const ErrorMessage(ErrorCode.documentBulkDeleteFailed); + throw const ErrorMessage(ErrorCode.documentBulkActionFailed); } } diff --git a/lib/features/documents/view/pages/document_details_page.dart b/lib/features/documents/view/pages/document_details_page.dart index e95bb66..20c54da 100644 --- a/lib/features/documents/view/pages/document_details_page.dart +++ b/lib/features/documents/view/pages/document_details_page.dart @@ -5,7 +5,8 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; @@ -345,16 +346,20 @@ class _DocumentDetailsPageState extends State { return const SizedBox(height: 32.0); } - void _onEdit(DocumentModel document) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => LabelBlocProvider( - child: DocumentEditPage(document: document), - ), - maintainState: true, - ), - ); + void _onEdit(DocumentModel document) async { + final wasUpdated = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LabelBlocProvider( + child: DocumentEditPage(document: document), + ), + maintainState: true, + ), + ) ?? + false; + if (wasUpdated) { + BlocProvider.of(context).updateStatistics(); + } } Future _onDownload(DocumentModel document) async { diff --git a/lib/features/documents/view/pages/document_edit_page.dart b/lib/features/documents/view/pages/document_edit_page.dart index 5796567..7ca453a 100644 --- a/lib/features/documents/view/pages/document_edit_page.dart +++ b/lib/features/documents/view/pages/document_edit_page.dart @@ -3,6 +3,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:intl/intl.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -20,6 +22,7 @@ import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_co import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart'; @@ -27,8 +30,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie 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'; -import 'package:form_builder_validators/form_builder_validators.dart'; -import 'package:intl/intl.dart'; class DocumentEditPage extends StatefulWidget { final DocumentModel document; @@ -80,13 +81,15 @@ class _DocumentEditPageState extends State { setState(() { _isSubmitLoading = true; }); + bool wasUpdated = false; try { await getIt().updateDocument(updatedDocument); showSnackBar(context, S.of(context).documentUpdateErrorMessage); + wasUpdated = true; } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); } finally { - Navigator.pop(context); + Navigator.pop(context, wasUpdated); } } }, @@ -115,7 +118,7 @@ class _DocumentEditPageState extends State { child: ListView(children: [ _buildTitleFormField().padded(), _buildCreatedAtFormField().padded(), - BlocBuilder>( + BlocBuilder>( builder: (context, state) { return LabelFormField( notAssignedSelectable: false, @@ -130,7 +133,7 @@ class _DocumentEditPageState extends State { label: S.of(context).documentDocumentTypePropertyLabel, initialValue: DocumentTypeQuery.fromId(widget.document.documentType), - state: state, + state: state.labels, name: fkDocumentType, queryParameterIdBuilder: DocumentTypeQuery.fromId, queryParameterNotAssignedBuilder: @@ -139,7 +142,7 @@ class _DocumentEditPageState extends State { ); }, ).padded(), - BlocBuilder>( + BlocBuilder>( builder: (context, state) { return LabelFormField( notAssignedSelectable: false, @@ -150,7 +153,7 @@ class _DocumentEditPageState extends State { child: AddCorrespondentPage(initalValue: initialValue), ), label: S.of(context).documentCorrespondentPropertyLabel, - state: state, + state: state.labels, initialValue: CorrespondentQuery.fromId(widget.document.correspondent), name: fkCorrespondent, @@ -161,7 +164,7 @@ class _DocumentEditPageState extends State { ); }, ).padded(), - BlocBuilder>( + BlocBuilder>( builder: (context, state) { return LabelFormField( notAssignedSelectable: false, @@ -172,7 +175,7 @@ class _DocumentEditPageState extends State { child: AddStoragePathPage(initalValue: initialValue), ), label: S.of(context).documentStoragePathPropertyLabel, - state: state, + state: state.labels, initialValue: StoragePathQuery.fromId(widget.document.storagePath), name: fkStoragePath, diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 72f0948..44cd76c 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; @@ -237,6 +238,8 @@ class _DocumentsPageState extends State { BlocProvider.value(value: BlocProvider.of(context)), BlocProvider.value( value: BlocProvider.of(context)), + BlocProvider.value( + value: BlocProvider.of(context)), ], child: DocumentDetailsPage( documentId: model.id, 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 c9dab74..21b857d 100644 --- a/lib/features/documents/view/widgets/list/document_list_item.dart +++ b/lib/features/documents/view/widgets/list/document_list_item.dart @@ -43,7 +43,6 @@ class DocumentListItem extends StatelessWidget { child: CorrespondentWidget( isClickable: isLabelClickable, correspondentId: document.correspondent, - afterSelected: () {}, ), ), ], 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 2498fa1..564da9c 100644 --- a/lib/features/documents/view/widgets/search/document_filter_panel.dart +++ b/lib/features/documents/view/widgets/search/document_filter_panel.dart @@ -18,6 +18,7 @@ import 'package:paperless_mobile/features/labels/correspondent/bloc/corresponden import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; @@ -53,14 +54,6 @@ class _DocumentFilterPanelState extends State { final _formKey = GlobalKey(); - late final DocumentsCubit _documentsCubit; - - @override - void initState() { - super.initState(); - _documentsCubit = BlocProvider.of(context); - } - DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) { if (start == null && end == null) { return null; @@ -181,12 +174,12 @@ class _DocumentFilterPanelState extends State { } Widget _buildDocumentTypeFormField(DocumentsState docState) { - return BlocBuilder>( + return BlocBuilder>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkDocumentType, - state: state, + state: state.labels, label: S.of(context).documentDocumentTypePropertyLabel, initialValue: docState.filter.documentType, queryParameterIdBuilder: DocumentTypeQuery.fromId, @@ -198,12 +191,12 @@ class _DocumentFilterPanelState extends State { } Widget _buildCorrespondentFormField(DocumentsState docState) { - return BlocBuilder>( + return BlocBuilder>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkCorrespondent, - state: state, + state: state.labels, label: S.of(context).documentCorrespondentPropertyLabel, initialValue: docState.filter.correspondent, queryParameterIdBuilder: CorrespondentQuery.fromId, @@ -215,12 +208,12 @@ class _DocumentFilterPanelState extends State { } Widget _buildStoragePathFormField(DocumentsState docState) { - return BlocBuilder>( + return BlocBuilder>( builder: (context, state) { return LabelFormField( formBuilderState: _formKey.currentState, name: fkStoragePath, - state: state, + state: state.labels, label: S.of(context).documentStoragePathPropertyLabel, initialValue: docState.filter.storagePath, queryParameterIdBuilder: StoragePathQuery.fromId, diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index b257425..fc05393 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -1,18 +1,24 @@ +import 'dart:developer'; +import 'dart:isolate'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; 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/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.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/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; -import 'package:paperless_mobile/features/documents/repository/document_repository_impl.dart'; import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart'; import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/inbox/view/inbox_page.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; @@ -20,6 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; +import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/util.dart'; class HomePage extends StatefulWidget { @@ -35,7 +42,30 @@ class _HomePageState extends State { @override void initState() { super.initState(); - _initializeData(context); + _initializeData(context).then( + (_) async { + FlutterNativeSplash.remove(); + if (BlocProvider.of(context) + .state + .showInboxOnStartup) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BlocProvider.value( + value: getIt(), + child: LabelBlocProvider( + child: BlocProvider.value( + value: DocumentsCubit(getIt()), + child: const InboxPage(), + ), + ), + ), + ), + ); + getIt().reloadDocuments(); + } + }, + ); } @override @@ -59,10 +89,6 @@ class _HomePageState extends State { ), drawer: const InfoDrawer(), body: [ - BlocProvider.value( - value: DocumentsCubit(getIt()), - child: const InboxPage(), - ), BlocProvider.value( value: getIt(), child: const DocumentsPage(), @@ -78,17 +104,21 @@ class _HomePageState extends State { ); } - _initializeData(BuildContext context) async { + Future _initializeData(BuildContext context) { try { - await BlocProvider.of(context) - .updateStatus(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); - BlocProvider.of(context).initialize(); + return Future.wait([ + BlocProvider.of(context) + .updateInformtion(), + BlocProvider.of(context).updateStatistics(), + BlocProvider.of(context).initialize(), + BlocProvider.of(context).initialize(), + BlocProvider.of(context).initialize(), + BlocProvider.of(context).initialize(), + BlocProvider.of(context).initialize(), + ]); } on ErrorMessage catch (error, stackTrace) { showErrorMessage(context, error, stackTrace); + return Future.error(error); } } } diff --git a/lib/features/home/view/widget/bottom_navigation_bar.dart b/lib/features/home/view/widget/bottom_navigation_bar.dart index 24dd4be..d7ed2a8 100644 --- a/lib/features/home/view/widget/bottom_navigation_bar.dart +++ b/lib/features/home/view/widget/bottom_navigation_bar.dart @@ -18,14 +18,6 @@ class BottomNavBar extends StatelessWidget { onDestinationSelected: onNavigationChanged, selectedIndex: selectedIndex, destinations: [ - NavigationDestination( - icon: const Icon(Icons.inbox_outlined), - selectedIcon: Icon( - Icons.inbox, - color: Theme.of(context).colorScheme.primary, - ), - label: S.of(context).bottomNavInboxPageLabel, - ), NavigationDestination( icon: const Icon(Icons.description_outlined), selectedIcon: Icon( diff --git a/lib/features/home/view/widget/info_drawer.dart b/lib/features/home/view/widget/info_drawer.dart index bb10d82..72e839d 100644 --- a/lib/features/home/view/widget/info_drawer.dart +++ b/lib/features/home/view/widget/info_drawer.dart @@ -1,7 +1,9 @@ import 'package:badges/badges.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics_state.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/paperless_server_information.dart'; @@ -57,7 +59,7 @@ class InfoDrawer extends StatelessWidget { ).padded(const EdgeInsets.only(right: 8.0)), Text( S.of(context).appTitleText, - style: Theme.of(context).textTheme.headline5!.copyWith( + style: Theme.of(context).textTheme.headline5?.copyWith( color: Theme.of(context) .colorScheme .onPrimaryContainer, @@ -104,18 +106,6 @@ class InfoDrawer extends StatelessWidget { ), ], ), - // title: RichText( - - // text: TextSpan( - // children: [ - // TextSpan( - // text: - // style: - // Theme.of(context).textTheme.bodyText2, - // ), - // ], - // ), - // ), isThreeLine: true, ), ], @@ -129,27 +119,32 @@ class InfoDrawer extends StatelessWidget { color: Theme.of(context).colorScheme.primaryContainer, ), ), - FutureBuilder( - future: getIt().getStatistics(), - builder: (context, snapshot) { + BlocBuilder( + builder: (context, state) { return ListTile( - title: Text("Inbox"), + title: Text(S.of(context).bottomNavInboxPageLabel), leading: const Icon(Icons.inbox), - trailing: snapshot.hasData - ? Text( - snapshot.data!.documentsInInbox.toString(), - ) + trailing: state.isLoaded + ? Text(state.statistics!.documentsInInbox.toString()) : null, - onTap: () => Navigator.push( + onTap: () async { + await Navigator.push( context, MaterialPageRoute( - builder: (context) => LabelBlocProvider( - child: BlocProvider.value( - value: DocumentsCubit(getIt()), - child: const InboxPage(), + builder: (context) => BlocProvider.value( + value: getIt(), + child: LabelBlocProvider( + child: BlocProvider.value( + value: + DocumentsCubit(getIt()), + child: const InboxPage(), + ), ), ), - )), + ), + ); + getIt().reloadDocuments(); + }, ); }, ), diff --git a/lib/features/inbox/view/inbox_page.dart b/lib/features/inbox/view/inbox_page.dart index 4a5ab82..8cd7c42 100644 --- a/lib/features/inbox/view/inbox_page.dart +++ b/lib/features/inbox/view/inbox_page.dart @@ -1,18 +1,25 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; +import 'package:paperless_mobile/core/model/error_message.dart'; +import 'package:paperless_mobile/core/model/paperless_statistics_state.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/bloc/documents_state.dart'; +import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; -import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; -import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; +import 'package:paperless_mobile/generated/l10n.dart'; +import 'package:paperless_mobile/util.dart'; class InboxPage extends StatefulWidget { const InboxPage({super.key}); @@ -22,6 +29,9 @@ class InboxPage extends StatefulWidget { } class _InboxPageState extends State { + static const _a4AspectRatio = 1 / 1.4142; + + final GlobalKey _listKey = GlobalKey(); Iterable _inboxTags = []; @override void initState() { @@ -31,9 +41,12 @@ class _InboxPageState extends State { } Future _initInbox() async { - final tags = BlocProvider.of(context).state.values; - _inboxTags = tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!); - final filter = DocumentFilter(tags: IdsTagsQuery.included(_inboxTags)); + final tags = BlocProvider.of(context).state.labels; + log("Loading documents with tags...${tags.values.join(",")}"); + _inboxTags = + tags.values.where((t) => t.isInboxTag ?? false).map((t) => t.id!); + final filter = + DocumentFilter(tags: AnyAssignedTagsQuery(tagIds: _inboxTags)); return BlocProvider.of(context).updateFilter( filter: filter, ); @@ -41,71 +54,210 @@ class _InboxPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Inbox"), - ), - drawer: const InfoDrawer(), - floatingActionButton: FloatingActionButton.extended( - label: Text("Mark all as read"), - icon: const Icon(FontAwesomeIcons.checkDouble), - onPressed: () {}, - ), - body: BlocBuilder( - builder: (context, state) { - if (!state.isLoaded) { - return const Center(child: CircularProgressIndicator()); - } - if (state.documents.isEmpty) { - return Text("You do not have new documents in your inbox.") - .padded(); - } - return Column( - children: [ - Text( - "You have ${state.documents.length} documents in your inbox.", + return BlocBuilder( + builder: (context, documentState) { + return Scaffold( + appBar: AppBar( + title: + BlocBuilder( + builder: (context, state) { + return Text( + S.of(context).bottomNavInboxPageLabel + + (state.isLoaded + ? ' (${state.statistics!.documentsInInbox})' + : ''), + ); + }, + ), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ), + floatingActionButton: documentState.documents.isNotEmpty + ? FloatingActionButton.extended( + label: Text("Mark all as seen"), + icon: const Icon(Icons.done_all), + onPressed: () => + _onMarkAllAsSeen(documentState.documents, _inboxTags), + ) + : null, + body: Builder( + builder: (context) { + if (!documentState.isLoaded) { + return const Center(child: CircularProgressIndicator()); + } + if (documentState.documents.isEmpty) { + return Text( + "You do not have new documents in your inbox.", + textAlign: TextAlign.center, + ) // TODO: INTL + .padded(); + } + return Column( + children: [ + Text( + 'Hint: Swipe left to mark a document as read. This will remove all inbox tags from the document.', //TODO: INTL + style: Theme.of(context).textTheme.caption, + ).padded( + const EdgeInsets.only( + top: 4.0, + left: 8.0, + right: 8.0, + bottom: 8.0, + ), + ), + Expanded( + child: AnimatedList( + key: _listKey, + initialItemCount: documentState.documents.length, + itemBuilder: (context, index, animation) { + final doc = documentState.documents[index]; + return _buildListItem(context, doc); + }, + ), + ), + ], + ); + }, + ), + ); + }, + ); + } + + Widget _buildListItem(BuildContext context, DocumentModel doc) { + return Dismissible( + direction: DismissDirection.endToStart, + background: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + Icons.done, + color: Theme.of(context).colorScheme.primary, + ).padded(), + Text( + 'Mark as read', //TODO: INTL + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ).padded(), + confirmDismiss: (_) => _onItemDismissed(doc), + key: ObjectKey(doc.id), + child: ListTile( + title: Text(doc.title), + isThreeLine: true, + leading: AspectRatio( + aspectRatio: _a4AspectRatio, + child: DocumentPreview( + id: doc.id, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat().format(doc.added)), + TagsWidget(tagIds: doc.tags.where((id) => _inboxTags.contains(id))) + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LabelBlocProvider( + child: BlocProvider.value( + value: BlocProvider.of(context), + child: DocumentDetailsPage( + documentId: doc.id, + allowEdit: false, + isLabelClickable: false, + ), ), - Expanded( - child: ListView( - children: state.documents - .map( - (doc) => Dismissible( - direction: DismissDirection.endToStart, - onDismissed: (_) { - BlocProvider.of(context) - .removeInboxTags(doc, _inboxTags); - }, - key: ObjectKey(doc.id), - child: ListTile( - title: Text(doc.title), - isThreeLine: true, - leading: DocumentPreview(id: doc.id), - subtitle: Text(DateFormat().format(doc.added)), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => LabelBlocProvider( - child: BlocProvider.value( - value: - BlocProvider.of(context), - child: DocumentDetailsPage( - documentId: doc.id, - allowEdit: false, - isLabelClickable: false, - ), - ), - ), - ), - ), - ), - ), - ) - .toList(), - )), - ], - ); - }, + ), + ), + ), ), ); } + + Widget _buildSlideAnimation( + BuildContext context, + animation, + Widget child, + ) { + return SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ); + } + + Future _onMarkAllAsSeen( + List documents, + Iterable inboxTags, + ) async { + for (int i = documents.length - 1; i >= 0; i--) { + final doc = documents[i]; + _listKey.currentState?.removeItem( + 0, + (context, animation) => _buildSlideAnimation( + context, + animation, + _buildListItem(context, doc), + ), + ); + await Future.delayed(const Duration(milliseconds: 75)); + } + await BlocProvider.of(context) + .bulkEditTags(documents, removeTags: inboxTags); + BlocProvider.of(context).resetInboxCount(); + } + + Future _onItemDismissed(DocumentModel doc) async { + try { + final removedTags = await BlocProvider.of(context) + .removeInboxTags(doc, _inboxTags); + BlocProvider.of(context).decrementInboxCount(); + showSnackBar( + context, + 'Document removed from inbox.', //TODO: INTL + action: SnackBarAction( + label: 'UNDO', //TODO: INTL + textColor: Theme.of(context).colorScheme.primary, + onPressed: () => _onUndoMarkAsSeen(doc, removedTags), + ), + ); + return true; + } on ErrorMessage catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + return false; + } catch (error) { + showErrorMessage( + context, + const ErrorMessage.unknown(), + ); + return false; + } + } + + Future _onUndoMarkAsSeen( + DocumentModel doc, Iterable removedTags) async { + try { + await BlocProvider.of(context).updateDocument( + doc.copyWith( + tags: {...doc.tags, ...removedTags}, + overwriteTags: true, + ), + ); + BlocProvider.of(context).incrementInboxCount(); + BlocProvider.of(context).reloadDocuments(); + } on ErrorMessage catch (error, stackTrace) { + showErrorMessage(context, error, stackTrace); + } + } } diff --git a/lib/core/bloc/label_bloc_provider.dart b/lib/features/labels/bloc/label_bloc_provider.dart similarity index 100% rename from lib/core/bloc/label_bloc_provider.dart rename to lib/features/labels/bloc/label_bloc_provider.dart diff --git a/lib/features/labels/bloc/label_cubit.dart b/lib/features/labels/bloc/label_cubit.dart new file mode 100644 index 0000000..c4bc823 --- /dev/null +++ b/lib/features/labels/bloc/label_cubit.dart @@ -0,0 +1,75 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/features/labels/model/label.model.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; +import 'package:paperless_mobile/features/labels/repository/label_repository.dart'; + +abstract class LabelCubit extends Cubit> { + final LabelRepository labelRepository; + + LabelCubit(this.labelRepository) : super(LabelState.initial()); + + @protected + void loadFrom(Iterable items) { + emit( + LabelState( + isLoaded: true, + labels: Map.fromIterable(items, key: (e) => (e as T).id!), + ), + ); + } + + Future add(T item) async { + assert(item.id == null); + final addedItem = await save(item); + final newValues = {...state.labels}; + newValues.putIfAbsent(addedItem.id!, () => addedItem); + emit( + LabelState( + isLoaded: true, + labels: newValues, + ), + ); + return addedItem; + } + + Future replace(T item) async { + assert(item.id != null); + final updatedItem = await update(item); + final updatedValues = {...state.labels}; + updatedValues[item.id!] = updatedItem; + emit( + LabelState( + isLoaded: state.isLoaded, + labels: updatedValues, + ), + ); + return updatedItem; + } + + Future remove(T item) async { + assert(item.id != null); + if (state.labels.containsKey(item.id)) { + final deletedId = await delete(item); + final updatedValues = {...state.labels}..remove(deletedId); + emit( + LabelState(isLoaded: true, labels: updatedValues), + ); + } + } + + void reset() { + emit(LabelState(isLoaded: false, labels: {})); + } + + Future initialize(); + + @protected + Future save(T item); + + @protected + Future update(T item); + + @protected + Future delete(T item); +} diff --git a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart b/lib/features/labels/correspondent/bloc/correspondents_cubit.dart index 03c62a2..6bb70c9 100644 --- a/lib/features/labels/correspondent/bloc/correspondents_cubit.dart +++ b/lib/features/labels/correspondent/bloc/correspondents_cubit.dart @@ -1,4 +1,4 @@ -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; import 'package:injectable/injectable.dart'; diff --git a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart index 5e5a0e0..e499e91 100644 --- a/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart +++ b/lib/features/labels/correspondent/view/widgets/correspondent_widget.dart @@ -6,6 +6,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/util.dart'; class CorrespondentWidget extends StatelessWidget { @@ -16,7 +17,7 @@ class CorrespondentWidget extends StatelessWidget { const CorrespondentWidget({ Key? key, - required this.correspondentId, + this.correspondentId, this.afterSelected, this.textColor, this.isClickable = true, @@ -26,12 +27,12 @@ class CorrespondentWidget extends StatelessWidget { Widget build(BuildContext context) { return AbsorbPointer( absorbing: !isClickable, - child: BlocBuilder>( + child: BlocBuilder>( builder: (context, state) { return GestureDetector( onTap: () => _addCorrespondentToFilter(context), child: Text( - (state[correspondentId]?.name) ?? "-", + (state.getLabel(correspondentId)?.name) ?? "-", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyText2?.copyWith( diff --git a/lib/features/labels/document_type/bloc/document_type_cubit.dart b/lib/features/labels/document_type/bloc/document_type_cubit.dart index ca534cc..f10a6b6 100644 --- a/lib/features/labels/document_type/bloc/document_type_cubit.dart +++ b/lib/features/labels/document_type/bloc/document_type_cubit.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:injectable/injectable.dart'; 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 8500adc..9655ce3 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 @@ -5,6 +5,7 @@ import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/util.dart'; class DocumentTypeWidget extends StatelessWidget { @@ -24,10 +25,10 @@ class DocumentTypeWidget extends StatelessWidget { absorbing: !isClickable, child: GestureDetector( onTap: () => _addDocumentTypeToFilter(context), - child: BlocBuilder>( + child: BlocBuilder>( builder: (context, state) { return Text( - state[documentTypeId]?.toString() ?? "-", + state.labels[documentTypeId]?.toString() ?? "-", style: Theme.of(context) .textTheme .bodyText2! diff --git a/lib/features/labels/model/label_state.dart b/lib/features/labels/model/label_state.dart new file mode 100644 index 0000000..b47ae48 --- /dev/null +++ b/lib/features/labels/model/label_state.dart @@ -0,0 +1,19 @@ +import 'package:paperless_mobile/features/labels/model/label.model.dart'; + +class LabelState { + LabelState.initial() : this(isLoaded: false, labels: {}); + final bool isLoaded; + final Map labels; + + LabelState({ + required this.isLoaded, + required this.labels, + }); + + T? getLabel(int? key) { + if (isLoaded) { + return labels[key]; + } + return null; + } +} diff --git a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart b/lib/features/labels/storage_path/bloc/storage_path_cubit.dart index efbd203..a2ac69d 100644 --- a/lib/features/labels/storage_path/bloc/storage_path_cubit.dart +++ b/lib/features/labels/storage_path/bloc/storage_path_cubit.dart @@ -1,5 +1,5 @@ import 'package:injectable/injectable.dart'; -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; @singleton 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 d713585..a554e60 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 @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/util.dart'; @@ -15,7 +16,7 @@ class StoragePathWidget extends StatelessWidget { const StoragePathWidget({ Key? key, - required this.pathId, + this.pathId, this.afterSelected, this.textColor, this.isClickable = true, @@ -25,12 +26,12 @@ class StoragePathWidget extends StatelessWidget { Widget build(BuildContext context) { return AbsorbPointer( absorbing: !isClickable, - child: BlocBuilder>( + child: BlocBuilder>( builder: (context, state) { return GestureDetector( onTap: () => _addStoragePathToFilter(context), child: Text( - (state[pathId]?.name) ?? "-", + state.getLabel(pathId)?.name ?? "-", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyText2?.copyWith( diff --git a/lib/features/labels/tags/bloc/tags_cubit.dart b/lib/features/labels/tags/bloc/tags_cubit.dart index 19e2611..4af8bed 100644 --- a/lib/features/labels/tags/bloc/tags_cubit.dart +++ b/lib/features/labels/tags/bloc/tags_cubit.dart @@ -1,4 +1,4 @@ -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:injectable/injectable.dart'; diff --git a/lib/features/labels/tags/view/pages/edit_tag_page.dart b/lib/features/labels/tags/view/pages/edit_tag_page.dart index 39bcf33..e9e9d2b 100644 --- a/lib/features/labels/tags/view/pages/edit_tag_page.dart +++ b/lib/features/labels/tags/view/pages/edit_tag_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; @@ -21,7 +22,11 @@ class EditTagPage extends StatelessWidget { Widget build(BuildContext context) { return EditLabelPage( label: tag, - onSubmit: BlocProvider.of(context).replace, + onSubmit: (tag) async { + await BlocProvider.of(context).replace(tag); + //If inbox property was added/removed from tag, the number of documetns in inbox may increase/decrease. + BlocProvider.of(context).updateStatistics(); + }, onDelete: (tag) => _onDelete(tag, context), fromJson: Tag.fromJson, additionalFields: [ 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 90dcd84..5f02490 100644 --- a/lib/features/labels/tags/view/widgets/tags_form_field.dart +++ b/lib/features/labels/tags/view/widgets/tags_form_field.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart'; @@ -45,7 +46,7 @@ class _TagFormFieldState extends State { _textEditingController = TextEditingController() ..addListener(() { setState(() { - _showCreationSuffixIcon = state.values + _showCreationSuffixIcon = state.labels.values .where( (item) => item.name.toLowerCase().startsWith( _textEditingController.text.toLowerCase(), @@ -61,7 +62,7 @@ class _TagFormFieldState extends State { @override Widget build(BuildContext context) { - return BlocBuilder>( + return BlocBuilder>( builder: (context, tagState) { return FormBuilderField( builder: (field) { @@ -81,7 +82,7 @@ class _TagFormFieldState extends State { controller: _textEditingController, ), suggestionsCallback: (query) { - final suggestions = tagState.values + final suggestions = tagState.labels.values .where((element) => element.name .toLowerCase() .startsWith(query.toLowerCase())) @@ -113,7 +114,7 @@ class _TagFormFieldState extends State { title: Text(S.of(context).labelAnyAssignedText), ); } - final tag = tagState[data]!; + final tag = tagState.getLabel(data)!; return ListTile( leading: Icon( Icons.circle, @@ -159,7 +160,7 @@ class _TagFormFieldState extends State { (query) => _buildTag( field, query, - tagState[query.id]!, + tagState.getLabel(query.id)!, ), ) .toList(), diff --git a/lib/features/labels/tags/view/widgets/tags_widget.dart b/lib/features/labels/tags/view/widgets/tags_widget.dart index c0b0837..6d09178 100644 --- a/lib/features/labels/tags/view/widgets/tags_widget.dart +++ b/lib/features/labels/tags/view/widgets/tags_widget.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; @@ -27,13 +28,13 @@ class TagsWidget extends StatefulWidget { class _TagsWidgetState extends State { @override Widget build(BuildContext context) { - return BlocBuilder>( + return BlocBuilder>( builder: (context, state) { final children = widget.tagIds - .where((id) => state.containsKey(id)) + .where((id) => state.labels.containsKey(id)) .map( (id) => TagWidget( - tag: state[id]!, + tag: state.getLabel(id)!, afterTagTapped: widget.afterTagTapped, isClickable: widget.isClickable, ), diff --git a/lib/features/labels/view/pages/add_label_page.dart b/lib/features/labels/view/pages/add_label_page.dart index 73064ee..7f0bf61 100644 --- a/lib/features/labels/view/pages/add_label_page.dart +++ b/lib/features/labels/view/pages/add_label_page.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/type/types.dart'; diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index a9ed051..165e305 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; @@ -227,6 +228,7 @@ class _LabelsPageState extends State providers: [ BlocProvider.value(value: getIt()), BlocProvider.value(value: BlocProvider.of(context)), + BlocProvider.value(value: getIt()), ], child: EditTagPage(tag: tag), ), diff --git a/lib/features/labels/view/widgets/label_item.dart b/lib/features/labels/view/widgets/label_item.dart index 0f4e784..7104ced 100644 --- a/lib/features/labels/view/widgets/label_item.dart +++ b/lib/features/labels/view/widgets/label_item.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/di_initializer.dart'; diff --git a/lib/features/labels/view/widgets/label_tab_view.dart b/lib/features/labels/view/widgets/label_tab_view.dart index 951e9cb..f853bb0 100644 --- a/lib/features/labels/view/widgets/label_tab_view.dart +++ b/lib/features/labels/view/widgets/label_tab_view.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/features/documents/model/document_filter.dart'; import 'package:paperless_mobile/features/labels/model/label.model.dart'; +import 'package:paperless_mobile/features/labels/model/label_state.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -45,10 +46,10 @@ class LabelTabView extends StatelessWidget { } return RefreshIndicator( onRefresh: cubit.initialize, - child: BlocBuilder>, Map>( + child: BlocBuilder>, LabelState>( bloc: cubit, builder: (context, state) { - final labels = state.values.toList()..sort(); + final labels = state.labels.values.toList()..sort(); if (labels.isEmpty) { return Center( child: Column( diff --git a/lib/features/labels/view/widgets/linked_documents_preview.dart b/lib/features/labels/view/widgets/linked_documents_preview.dart index 5aeded0..8ab6c6d 100644 --- a/lib/features/labels/view/widgets/linked_documents_preview.dart +++ b/lib/features/labels/view/widgets/linked_documents_preview.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart'; diff --git a/lib/features/scan/view/document_upload_page.dart b/lib/features/scan/view/document_upload_page.dart index 0b62cc1..ed9a3fd 100644 --- a/lib/features/scan/view/document_upload_page.dart +++ b/lib/features/scan/view/document_upload_page.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/di_initializer.dart'; @@ -19,6 +20,7 @@ import 'package:paperless_mobile/features/labels/correspondent/model/corresponde import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart'; +import 'package:paperless_mobile/features/labels/model/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/scan/bloc/document_scanner_cubit.dart'; @@ -165,7 +167,7 @@ class _DocumentUploadPageState extends State { labelText: S.of(context).documentCreatedPropertyLabel + " *", ), ), - BlocBuilder>( + BlocBuilder>( bloc: getIt(), //TODO: Use provider builder: (context, state) { return LabelFormField( @@ -178,7 +180,7 @@ class _DocumentUploadPageState extends State { ), label: S.of(context).documentDocumentTypePropertyLabel + " *", name: DocumentModel.documentTypeKey, - state: state, + state: state.labels, queryParameterIdBuilder: DocumentTypeQuery.fromId, queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, @@ -186,7 +188,7 @@ class _DocumentUploadPageState extends State { ); }, ), - BlocBuilder>( + BlocBuilder>( bloc: getIt(), //TODO: Use provider builder: (context, state) { return LabelFormField( @@ -200,7 +202,7 @@ class _DocumentUploadPageState extends State { label: S.of(context).documentCorrespondentPropertyLabel + " *", name: DocumentModel.correspondentKey, - state: state, + state: state.labels, queryParameterIdBuilder: CorrespondentQuery.fromId, queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, @@ -257,7 +259,7 @@ class _DocumentUploadPageState extends State { } on PaperlessValidationErrors catch (errorMessages) { setState(() => _errors = errorMessages); } catch (unknownError, stackTrace) { - showErrorMessage(context, ErrorMessage.unknown(), stackTrace); + showErrorMessage(context, const ErrorMessage.unknown(), stackTrace); } finally { setState(() { _isUploadLoading = false; @@ -274,22 +276,23 @@ class _DocumentUploadPageState extends State { return source.replaceAll(RegExp(r"[\W_]"), "_"); } - void _onConsumptionFinished(document) { - ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar( - SnackBar( - action: SnackBarAction( - onPressed: () async { - try { - getIt().reloadDocuments(); - } on ErrorMessage catch (error, stackTrace) { - showErrorMessage(context, error, stackTrace); - } - }, - label: - S.of(context).documentUploadProcessingSuccessfulReloadActionText, - ), - content: Text(S.of(context).documentUploadProcessingSuccessfulText), - ), - ); + void _onConsumptionFinished(DocumentModel document) { + // ScaffoldMessenger.of(rootScaffoldKey.currentContext!).showSnackBar( + // SnackBar( + // action: SnackBarAction( + // onPressed: () async { + // try { + // getIt().reloadDocuments(); + // } on ErrorMessage catch (error, stackTrace) { + // showErrorMessage(context, error, stackTrace); + // } + // }, + // label: + // S.of(context).documentUploadProcessingSuccessfulReloadActionText, + // ), + // content: Text(S.of(context).documentUploadProcessingSuccessfulText), + // ), + // ); + getIt().incrementInboxCount(); } } diff --git a/lib/features/scan/view/scanner_page.dart b/lib/features/scan/view/scanner_page.dart index c51fe4d..12ffcba 100644 --- a/lib/features/scan/view/scanner_page.dart +++ b/lib/features/scan/view/scanner_page.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:mime/mime.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; diff --git a/lib/features/settings/model/application_settings_state.dart b/lib/features/settings/model/application_settings_state.dart index 31924a1..c3a963b 100644 --- a/lib/features/settings/model/application_settings_state.dart +++ b/lib/features/settings/model/application_settings_state.dart @@ -14,23 +14,27 @@ class ApplicationSettingsState { preferredLocaleSubtag: Platform.localeName.split('_').first, preferredThemeMode: ThemeMode.system, preferredViewType: ViewType.list, + showInboxOnStartup: true, ); static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled"; static const preferredLocaleSubtagKey = "localeSubtag"; static const preferredThemeModeKey = "preferredThemeModeKey"; static const preferredViewTypeKey = 'preferredViewType'; + static const showInboxOnStartupKey = 'showinboxOnStartup'; final bool isLocalAuthenticationEnabled; final String preferredLocaleSubtag; final ThemeMode preferredThemeMode; final ViewType preferredViewType; + final bool showInboxOnStartup; ApplicationSettingsState({ required this.preferredLocaleSubtag, required this.preferredThemeMode, required this.isLocalAuthenticationEnabled, required this.preferredViewType, + required this.showInboxOnStartup, }); JSON toJson() { @@ -43,17 +47,25 @@ class ApplicationSettingsState { } ApplicationSettingsState.fromJson(JSON json) - : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey], - preferredLocaleSubtag = json[preferredLocaleSubtagKey], - preferredThemeMode = - ThemeMode.values.byName(json[preferredThemeModeKey]), - preferredViewType = ViewType.values.byName(json[preferredViewTypeKey]); + : isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ?? + defaultSettings.isLocalAuthenticationEnabled, + preferredLocaleSubtag = json[preferredLocaleSubtagKey] ?? + defaultSettings.preferredLocaleSubtag, + preferredThemeMode = json.containsKey(preferredThemeModeKey) + ? ThemeMode.values.byName(json[preferredThemeModeKey]) + : defaultSettings.preferredThemeMode, + preferredViewType = json.containsKey(preferredViewTypeKey) + ? ViewType.values.byName(json[preferredViewTypeKey]) + : defaultSettings.preferredViewType, + showInboxOnStartup = + json[showInboxOnStartupKey] ?? defaultSettings.showInboxOnStartup; ApplicationSettingsState copyWith({ bool? isLocalAuthenticationEnabled, String? preferredLocaleSubtag, ThemeMode? preferredThemeMode, ViewType? preferredViewType, + bool? showInboxOnStartup, }) { return ApplicationSettingsState( isLocalAuthenticationEnabled: @@ -62,6 +74,7 @@ class ApplicationSettingsState { preferredLocaleSubtag ?? this.preferredLocaleSubtag, preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode, preferredViewType: preferredViewType ?? this.preferredViewType, + showInboxOnStartup: showInboxOnStartup ?? this.showInboxOnStartup, ); } } diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 596b8dc..d788459 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -84,7 +84,7 @@ "appSettingsBiometricAuthenticationLabel": "Biometrische Authentifizierung aktivieren", "appSettingsEnableBiometricAuthenticationReasonText": "Authentifizieren, um die biometrische Authentifizierung zu aktivieren.", "appSettingsDisableBiometricAuthenticationReasonText": "Authentifizieren, um die biometrische Authentifizierung zu deaktivieren.", - "errorMessageBulkDeleteDocumentsFailed": "Es ist ein Fehler beim massenhaften Löschen der Dokumente aufgetreten.", + "errorMessageBulkActionFailed": "Es ist ein Fehler beim massenhaften bearbeiten der Dokumente aufgetreten.", "errorMessageBiotmetricsNotSupported": "Biometrische Authentifizierung wird von diesem Gerät nicht unterstützt.", "errorMessageBiometricAuthenticationFailed": "Biometrische Authentifizierung fehlgeschlagen.", "errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index da133c3..8460fff 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -83,7 +83,7 @@ "documentPreviewPageTitle": "Preview", "appSettingsEnableBiometricAuthenticationReasonText": "Authenticate to enable biometric authentication", "appSettingsDisableBiometricAuthenticationReasonText": "Authenticate to disable biometric authentication", - "errorMessageBulkDeleteDocumentsFailed": "Could not bulk delete documents.", + "errorMessageBulkActionFailed": "Could not bulk edit documents.", "errorMessageBiotmetricsNotSupported": "Biometric authentication not supported on this device.", "errorMessageBiometricAuthenticationFailed": "Biometric authentication failed.", "errorMessageDeviceOffline": "Could not fetch data: You are not connected to the internet.", diff --git a/lib/main.dart b/lib/main.dart index b55319e..720a9ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,8 @@ import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; -import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; +import 'package:paperless_mobile/core/bloc/paperless_statistics_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/global/asset_images.dart'; import 'package:paperless_mobile/core/global/constants.dart'; @@ -194,15 +195,6 @@ class _AuthenticationWrapperState extends State { ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles); } - @override - void didChangeDependencies() { - FlutterNativeSplash.remove(); - for (var element in AssetImages.values) { - element.load(context); - } - super.didChangeDependencies(); - } - @override Widget build(BuildContext context) { return SafeArea( @@ -215,9 +207,6 @@ class _AuthenticationWrapperState extends State { final bool showIntroSlider = authState.isAuthenticated && !authState.wasLoginStored; if (showIntroSlider) { - for (final img in AssetImages.values) { - img.load(context); - } Navigator.push( context, MaterialPageRoute( @@ -229,10 +218,14 @@ class _AuthenticationWrapperState extends State { }, builder: (context, authentication) { if (authentication.isAuthenticated) { - return const LabelBlocProvider( - child: HomePage(), + return BlocProvider.value( + value: getIt(), + child: const LabelBlocProvider( + child: HomePage(), + ), ); } else { + FlutterNativeSplash.remove(); return const LoginPage(); } },