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 PaperlessValidationErrors = Map<String, String>;
typedef PaperlessLocalizedErrorMessage = String;

View File

@@ -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<DocumentSearchState>
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<void> search(String query) async {
emit(state.copyWith(

View File

@@ -13,7 +13,10 @@ Future<void> 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(),
),
),

View File

@@ -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<DocumentsState>
@override
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 {
log("[DocumentsCubit] bulkRemove");

View File

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

View File

@@ -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<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,
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),
);
}

View File

@@ -211,10 +211,15 @@ class _HomePageState extends State<HomePage> {
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(),

View File

@@ -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<InboxState> with PagedDocumentsMixin {
_documentTypeRepository;
final PaperlessDocumentsApi _documentsApi;
@override
final DocumentChangedNotifier notifier;
final PaperlessServerStatsApi _statsApi;
@@ -32,6 +35,7 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
this._correspondentRepository,
this._documentTypeRepository,
this._statsApi,
this.notifier,
) : super(
InboxState(
availableCorrespondents:
@@ -41,6 +45,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
availableTags: _tagsRepository.current?.values ?? {},
),
) {
_subscriptions.addAll(
notifier.listen(
onDeleted: remove,
onUpdated: replace,
),
);
_subscriptions.add(
_tagsRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {

View File

@@ -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<LinkedDocumentsState>
@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);
}
}

View File

@@ -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<State extends PagedDocumentsState>
on BlocBase<State> {
PaperlessDocumentsApi get api;
DocumentChangedNotifier get notifier;
Future<void> loadMore() async {
if (state.isLastPageLoaded) {

View File

@@ -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';

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/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<LocalNotificationService>.value(
value: localNotificationService,
),
Provider.value(value: DocumentChangedNotifier()),
],
child: MultiRepositoryProvider(
providers: [