mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 22:07:49 -06:00
Adds change detection mechanism for document changes
This commit is contained in:
38
lib/core/notifier/document_changed_notifier.dart
Normal file
38
lib/core/notifier/document_changed_notifier.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user