diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart new file mode 100644 index 0000000..6597d6f --- /dev/null +++ b/lib/core/notifier/document_changed_notifier.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:paperless_api/paperless_api.dart'; +import 'package:rxdart/subjects.dart'; + +typedef DocumentChangedCallback = void Function(DocumentModel document); + +class DocumentChangedNotifier { + final Subject _updated = PublishSubject(); + final Subject _deleted = PublishSubject(); + + void notifyUpdated(DocumentModel updated) { + _updated.add(updated); + } + + void notifyDeleted(DocumentModel deleted) { + _deleted.add(deleted); + } + + List listen({ + DocumentChangedCallback? onUpdated, + DocumentChangedCallback? onDeleted, + }) { + return [ + _updated.listen((value) { + onUpdated?.call(value); + }), + _updated.listen((value) { + onDeleted?.call(value); + }), + ]; + } + + void close() { + _updated.close(); + _deleted.close(); + } +} diff --git a/lib/core/type/types.dart b/lib/core/type/types.dart index 3ed65fa..a133cbf 100644 --- a/lib/core/type/types.dart +++ b/lib/core/type/types.dart @@ -1,3 +1,6 @@ +import 'package:paperless_api/paperless_api.dart'; +import 'package:rxdart/subjects.dart'; + typedef JSON = Map; typedef PaperlessValidationErrors = Map; typedef PaperlessLocalizedErrorMessage = String; diff --git a/lib/features/document_search/cubit/document_search_cubit.dart b/lib/features/document_search/cubit/document_search_cubit.dart index 2707926..79300f2 100644 --- a/lib/features/document_search/cubit/document_search_cubit.dart +++ b/lib/features/document_search/cubit/document_search_cubit.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -8,7 +9,11 @@ class DocumentSearchCubit extends HydratedCubit with PagedDocumentsMixin { @override final PaperlessDocumentsApi api; - DocumentSearchCubit(this.api) : super(const DocumentSearchState()); + @override + final DocumentChangedNotifier notifier; + + DocumentSearchCubit(this.api, this.notifier) + : super(const DocumentSearchState()); Future search(String query) async { emit(state.copyWith( diff --git a/lib/features/document_search/view/document_search_page.dart b/lib/features/document_search/view/document_search_page.dart index 71b94ad..2f0cf21 100644 --- a/lib/features/document_search/view/document_search_page.dart +++ b/lib/features/document_search/view/document_search_page.dart @@ -13,7 +13,10 @@ Future showDocumentSearchPage(BuildContext context) { return Navigator.of(context).push( MaterialPageRoute( builder: (context) => BlocProvider( - create: (context) => DocumentSearchCubit(context.read()), + create: (context) => DocumentSearchCubit( + context.read(), + context.read(), + ), child: const DocumentSearchPage(), ), ), diff --git a/lib/features/documents/bloc/documents_cubit.dart b/lib/features/documents/bloc/documents_cubit.dart index 41dc04b..bc7bd04 100644 --- a/lib/features/documents/bloc/documents_cubit.dart +++ b/lib/features/documents/bloc/documents_cubit.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -12,7 +13,12 @@ class DocumentsCubit extends HydratedCubit @override final PaperlessDocumentsApi api; - DocumentsCubit(this.api) : super(const DocumentsState()); + @override + final DocumentChangedNotifier notifier; + + DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) { + reload(); + } Future bulkRemove(List documents) async { log("[DocumentsCubit] bulkRemove"); diff --git a/lib/features/documents/view/pages/documents_page.dart b/lib/features/documents/view/pages/documents_page.dart index 201f1b7..0889c9a 100644 --- a/lib/features/documents/view/pages/documents_page.dart +++ b/lib/features/documents/view/pages/documents_page.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:badges/badges.dart' as b; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/documents/view/widgets/items/document_list_item.dart b/lib/features/documents/view/widgets/items/document_list_item.dart index f63f165..e76b802 100644 --- a/lib/features/documents/view/widgets/items/document_list_item.dart +++ b/lib/features/documents/view/widgets/items/document_list_item.dart @@ -1,6 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart'; +import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; +import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; @@ -24,60 +30,108 @@ class DocumentListItem extends DocumentItem { @override Widget build(BuildContext context) { - return ListTile( - dense: true, - selected: isSelected, - onTap: () => _onTap(), - selectedTileColor: Theme.of(context).colorScheme.inversePrimary, - onLongPress: () => onSelected?.call(document), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Row( - children: [ - AbsorbPointer( - absorbing: isSelectionActive, - child: CorrespondentWidget( - isClickable: isLabelClickable, - correspondentId: document.correspondent, - onSelected: onCorrespondentSelected, + return DocumentTypeBlocProvider( + child: ListTile( + dense: true, + selected: isSelected, + onTap: () => _onTap(), + selectedTileColor: Theme.of(context).colorScheme.inversePrimary, + onLongPress: () => onSelected?.call(document), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + children: [ + AbsorbPointer( + absorbing: isSelectionActive, + child: CorrespondentWidget( + isClickable: isLabelClickable, + correspondentId: document.correspondent, + onSelected: onCorrespondentSelected, + ), ), + ], + ), + Text( + document.title, + overflow: TextOverflow.ellipsis, + maxLines: document.tags.isEmpty ? 2 : 1, + ), + AbsorbPointer( + absorbing: isSelectionActive, + child: TagsWidget( + isClickable: isLabelClickable, + tagIds: document.tags, + isMultiLine: false, + onTagSelected: (id) => onTagSelected?.call(id), ), - ], - ), - Text( - document.title, - overflow: TextOverflow.ellipsis, - maxLines: document.tags.isEmpty ? 2 : 1, - ), - ], - ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: AbsorbPointer( - absorbing: isSelectionActive, - child: TagsWidget( - isClickable: isLabelClickable, - tagIds: document.tags, - isMultiLine: false, - onTagSelected: (id) => onTagSelected?.call(id), + ) + ], + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: + BlocBuilder, LabelState>( + builder: (context, docTypes) { + return RichText( + maxLines: 1, + overflow: TextOverflow.ellipsis, + text: TextSpan( + text: DateFormat.yMMMd().format(document.created), + style: Theme.of(context) + .textTheme + .labelSmall + ?.apply(color: Colors.grey), + children: document.documentType != null + ? [ + const TextSpan(text: '\u30FB'), + TextSpan( + text: + docTypes.labels[document.documentType]?.name, + ), + ] + : null, + ), + ); + }, + ) + // Row( + // children: [ + // Text( + // DateFormat.yMMMd().format(document.created), + // style: Theme.of(context) + // .textTheme + // .bodySmall + // ?.apply(color: Colors.grey), + // ), + // if (document.documentType != null) ...[ + // Text("\u30FB"), + // DocumentTypeWidget( + // documentTypeId: document.documentType, + // textStyle: Theme.of(context).textTheme.bodySmall?.apply( + // color: Colors.grey, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ], + // ], + // ), + ), + isThreeLine: document.tags.isNotEmpty, + leading: AspectRatio( + aspectRatio: _a4AspectRatio, + child: GestureDetector( + child: DocumentPreview( + id: document.id, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + enableHero: enableHeroAnimation, + ), ), ), + contentPadding: const EdgeInsets.all(8.0), ), - isThreeLine: document.tags.isNotEmpty, - leading: AspectRatio( - aspectRatio: _a4AspectRatio, - child: GestureDetector( - child: DocumentPreview( - id: document.id, - fit: BoxFit.cover, - alignment: Alignment.topCenter, - enableHero: enableHeroAnimation, - ), - ), - ), - contentPadding: const EdgeInsets.all(8.0), ); } diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/view/home_page.dart index ae5311b..0e9729f 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/view/home_page.dart @@ -211,10 +211,15 @@ class _HomePageState extends State { MultiBlocProvider( providers: [ BlocProvider( - create: (context) => DocumentsCubit(context.read()), + create: (context) => DocumentsCubit( + context.read(), + context.read(), + ), ), BlocProvider( - create: (context) => SavedViewCubit(context.read()), + create: (context) => SavedViewCubit( + context.read(), + ), ), ], child: const DocumentsPage(), diff --git a/lib/features/inbox/bloc/inbox_cubit.dart b/lib/features/inbox/bloc/inbox_cubit.dart index f35d57f..8ad1c81 100644 --- a/lib/features/inbox/bloc/inbox_cubit.dart +++ b/lib/features/inbox/bloc/inbox_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart'; @@ -17,6 +18,8 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { _documentTypeRepository; final PaperlessDocumentsApi _documentsApi; + @override + final DocumentChangedNotifier notifier; final PaperlessServerStatsApi _statsApi; @@ -32,6 +35,7 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { this._correspondentRepository, this._documentTypeRepository, this._statsApi, + this.notifier, ) : super( InboxState( availableCorrespondents: @@ -41,6 +45,12 @@ class InboxCubit extends HydratedCubit with PagedDocumentsMixin { availableTags: _tagsRepository.current?.values ?? {}, ), ) { + _subscriptions.addAll( + notifier.listen( + onDeleted: remove, + onUpdated: replace, + ), + ); _subscriptions.add( _tagsRepository.values.listen((event) { if (event?.hasLoaded ?? false) { diff --git a/lib/features/linked_documents/bloc/linked_documents_cubit.dart b/lib/features/linked_documents/bloc/linked_documents_cubit.dart index ffc9343..bd66d05 100644 --- a/lib/features/linked_documents/bloc/linked_documents_cubit.dart +++ b/lib/features/linked_documents/bloc/linked_documents_cubit.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; @@ -8,8 +9,14 @@ class LinkedDocumentsCubit extends Cubit @override final PaperlessDocumentsApi api; - LinkedDocumentsCubit(this.api, DocumentFilter filter) - : super(const LinkedDocumentsState()) { + @override + final DocumentChangedNotifier notifier; + + LinkedDocumentsCubit( + this.api, + DocumentFilter filter, + this.notifier, + ) : super(const LinkedDocumentsState()) { updateFilter(filter: filter); } } diff --git a/lib/features/paged_document_view/paged_documents_mixin.dart b/lib/features/paged_document_view/paged_documents_mixin.dart index 687d410..22bebe1 100644 --- a/lib/features/paged_document_view/paged_documents_mixin.dart +++ b/lib/features/paged_document_view/paged_documents_mixin.dart @@ -1,15 +1,18 @@ import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'model/paged_documents_state.dart'; /// -/// Mixin which can be used on cubits which handle documents. This implements all paging and filtering logic. +/// Mixin which can be used on cubits that handle documents. +/// This implements all paging and filtering logic. /// mixin PagedDocumentsMixin on BlocBase { PaperlessDocumentsApi get api; + DocumentChangedNotifier get notifier; Future loadMore() async { if (state.isLastPageLoaded) { diff --git a/lib/features/saved_view/view/saved_view_list.dart b/lib/features/saved_view/view/saved_view_list.dart index 0c4b7a4..2090c3d 100644 --- a/lib/features/saved_view/view/saved_view_list.dart +++ b/lib/features/saved_view/view/saved_view_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_details_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; diff --git a/lib/main.dart b/lib/main.dart index 4f51e8e..4239ff2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ 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/interceptor/dio_http_error_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart'; import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart'; @@ -168,6 +169,7 @@ void main() async { Provider.value( value: localNotificationService, ), + Provider.value(value: DocumentChangedNotifier()), ], child: MultiRepositoryProvider( providers: [ diff --git a/packages/paperless_api/lib/src/models/document_filter.dart b/packages/paperless_api/lib/src/models/document_filter.dart index f612cdd..d872076 100644 --- a/packages/paperless_api/lib/src/models/document_filter.dart +++ b/packages/paperless_api/lib/src/models/document_filter.dart @@ -143,6 +143,11 @@ class DocumentFilter extends Equatable { return newFilter; } + /// + /// Checks whether the properties of [document] match the current filter criteria. + /// + bool includes(DocumentModel document) {} + int get appliedFiltersCount => [ documentType != initial.documentType, correspondent != initial.correspondent,