Adds change detection mechanism for document changes

This commit is contained in:
Anton Stubenbord
2023-02-04 19:24:11 +01:00
parent 3f305ce1d6
commit 337c178be8
14 changed files with 201 additions and 57 deletions

View File

@@ -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<DocumentModel> _updated = PublishSubject();
final Subject<DocumentModel> _deleted = PublishSubject();
void notifyUpdated(DocumentModel updated) {
_updated.add(updated);
}
void notifyDeleted(DocumentModel deleted) {
_deleted.add(deleted);
}
List<StreamSubscription> listen({
DocumentChangedCallback? onUpdated,
DocumentChangedCallback? onDeleted,
}) {
return [
_updated.listen((value) {
onUpdated?.call(value);
}),
_updated.listen((value) {
onDeleted?.call(value);
}),
];
}
void close() {
_updated.close();
_deleted.close();
}
}

View File

@@ -1,3 +1,6 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:rxdart/subjects.dart';
typedef JSON = Map<String, dynamic>; typedef JSON = Map<String, dynamic>;
typedef PaperlessValidationErrors = Map<String, String>; typedef PaperlessValidationErrors = Map<String, String>;
typedef PaperlessLocalizedErrorMessage = String; typedef PaperlessLocalizedErrorMessage = String;

View File

@@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.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/document_search/cubit/document_search_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
@@ -8,7 +9,11 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
with PagedDocumentsMixin { with PagedDocumentsMixin {
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
DocumentSearchCubit(this.api) : super(const DocumentSearchState()); @override
final DocumentChangedNotifier notifier;
DocumentSearchCubit(this.api, this.notifier)
: super(const DocumentSearchState());
Future<void> search(String query) async { Future<void> search(String query) async {
emit(state.copyWith( emit(state.copyWith(

View File

@@ -13,7 +13,10 @@ Future<void> showDocumentSearchPage(BuildContext context) {
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BlocProvider( builder: (context) => BlocProvider(
create: (context) => DocumentSearchCubit(context.read()), create: (context) => DocumentSearchCubit(
context.read(),
context.read(),
),
child: const DocumentSearchPage(), child: const DocumentSearchPage(),
), ),
), ),

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.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/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
@@ -12,7 +13,12 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
DocumentsCubit(this.api) : super(const DocumentsState()); @override
final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
reload();
}
Future<void> bulkRemove(List<DocumentModel> documents) async { Future<void> bulkRemove(List<DocumentModel> documents) async {
log("[DocumentsCubit] bulkRemove"); log("[DocumentsCubit] bulkRemove");

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:badges/badges.dart' as b; import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View File

@@ -1,6 +1,12 @@
import 'package:flutter/material.dart'; 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/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.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/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
@@ -24,7 +30,8 @@ class DocumentListItem extends DocumentItem {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return DocumentTypeBlocProvider(
child: ListTile(
dense: true, dense: true,
selected: isSelected, selected: isSelected,
onTap: () => _onTap(), onTap: () => _onTap(),
@@ -51,11 +58,7 @@ class DocumentListItem extends DocumentItem {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: document.tags.isEmpty ? 2 : 1, maxLines: document.tags.isEmpty ? 2 : 1,
), ),
], AbsorbPointer(
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: AbsorbPointer(
absorbing: isSelectionActive, absorbing: isSelectionActive,
child: TagsWidget( child: TagsWidget(
isClickable: isLabelClickable, isClickable: isLabelClickable,
@@ -63,7 +66,57 @@ class DocumentListItem extends DocumentItem {
isMultiLine: false, isMultiLine: false,
onTagSelected: (id) => onTagSelected?.call(id), onTagSelected: (id) => onTagSelected?.call(id),
), ),
)
],
), ),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
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, isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio( leading: AspectRatio(
@@ -78,6 +131,7 @@ class DocumentListItem extends DocumentItem {
), ),
), ),
contentPadding: const EdgeInsets.all(8.0), contentPadding: const EdgeInsets.all(8.0),
),
); );
} }

View File

@@ -211,10 +211,15 @@ class _HomePageState extends State<HomePage> {
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => DocumentsCubit(context.read()), create: (context) => DocumentsCubit(
context.read(),
context.read(),
),
), ),
BlocProvider( BlocProvider(
create: (context) => SavedViewCubit(context.read()), create: (context) => SavedViewCubit(
context.read(),
),
), ),
], ],
child: const DocumentsPage(), child: const DocumentsPage(),

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.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/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.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'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
@@ -17,6 +18,8 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
_documentTypeRepository; _documentTypeRepository;
final PaperlessDocumentsApi _documentsApi; final PaperlessDocumentsApi _documentsApi;
@override
final DocumentChangedNotifier notifier;
final PaperlessServerStatsApi _statsApi; final PaperlessServerStatsApi _statsApi;
@@ -32,6 +35,7 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
this._correspondentRepository, this._correspondentRepository,
this._documentTypeRepository, this._documentTypeRepository,
this._statsApi, this._statsApi,
this.notifier,
) : super( ) : super(
InboxState( InboxState(
availableCorrespondents: availableCorrespondents:
@@ -41,6 +45,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
availableTags: _tagsRepository.current?.values ?? {}, availableTags: _tagsRepository.current?.values ?? {},
), ),
) { ) {
_subscriptions.addAll(
notifier.listen(
onDeleted: remove,
onUpdated: replace,
),
);
_subscriptions.add( _subscriptions.add(
_tagsRepository.values.listen((event) { _tagsRepository.values.listen((event) {
if (event?.hasLoaded ?? false) { if (event?.hasLoaded ?? false) {

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/linked_documents/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
@@ -8,8 +9,14 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
LinkedDocumentsCubit(this.api, DocumentFilter filter) @override
: super(const LinkedDocumentsState()) { final DocumentChangedNotifier notifier;
LinkedDocumentsCubit(
this.api,
DocumentFilter filter,
this.notifier,
) : super(const LinkedDocumentsState()) {
updateFilter(filter: filter); updateFilter(filter: filter);
} }
} }

View File

@@ -1,15 +1,18 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'model/paged_documents_state.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<State extends PagedDocumentsState> mixin PagedDocumentsMixin<State extends PagedDocumentsState>
on BlocBase<State> { on BlocBase<State> {
PaperlessDocumentsApi get api; PaperlessDocumentsApi get api;
DocumentChangedNotifier get notifier;
Future<void> loadMore() async { Future<void> loadMore() async {
if (state.isLastPageLoaded) { if (state.isLastPageLoaded) {

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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_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_details_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';

View File

@@ -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/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.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/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/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_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'; import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart';
@@ -168,6 +169,7 @@ void main() async {
Provider<LocalNotificationService>.value( Provider<LocalNotificationService>.value(
value: localNotificationService, value: localNotificationService,
), ),
Provider.value(value: DocumentChangedNotifier()),
], ],
child: MultiRepositoryProvider( child: MultiRepositoryProvider(
providers: [ providers: [

View File

@@ -143,6 +143,11 @@ class DocumentFilter extends Equatable {
return newFilter; return newFilter;
} }
///
/// Checks whether the properties of [document] match the current filter criteria.
///
bool includes(DocumentModel document) {}
int get appliedFiltersCount => [ int get appliedFiltersCount => [
documentType != initial.documentType, documentType != initial.documentType,
correspondent != initial.correspondent, correspondent != initial.correspondent,