Cleaned up code, implemented message queue to notify subscribers of document updates.

This commit is contained in:
Anton Stubenbord
2023-02-06 01:04:13 +01:00
parent 337c178be8
commit 4d7fab1839
111 changed files with 1412 additions and 1029 deletions

View File

@@ -68,11 +68,11 @@ android {
storePassword keystoreProperties['storePassword'] storePassword keystoreProperties['storePassword']
} }
} }
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.debug
} }
} }
} }

View File

@@ -35,7 +35,7 @@ PODS:
- DKPhotoGallery/Resource (0.0.17): - DKPhotoGallery/Resource (0.0.17):
- SDWebImage - SDWebImage
- SwiftyGif - SwiftyGif
- edge_detection (1.0.9): - edge_detection (1.1.1):
- Flutter - Flutter
- WeScan - WeScan
- file_picker (0.0.1): - file_picker (0.0.1):
@@ -44,6 +44,8 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1): - flutter_keyboard_visibility (0.0.1):
- Flutter - Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1): - flutter_native_splash (0.0.1):
- Flutter - Flutter
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
@@ -56,10 +58,13 @@ PODS:
- Flutter - Flutter
- local_auth_ios (0.0.1): - local_auth_ios (0.0.1):
- Flutter - Flutter
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_ios (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- pdfx (1.0.0): - pdfx (1.0.0):
- Flutter - Flutter
- permission_handler_apple (9.0.4): - permission_handler_apple (9.0.4):
@@ -72,8 +77,9 @@ PODS:
- SDWebImage/Core (5.13.5) - SDWebImage/Core (5.13.5)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_ios (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- sqflite (0.0.2): - sqflite (0.0.2):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
@@ -90,17 +96,19 @@ DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pdfx (from `.symlinks/plugins/pdfx/ios`) - pdfx (from `.symlinks/plugins/pdfx/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -128,6 +136,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_keyboard_visibility: flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios" :path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
fluttertoast: fluttertoast:
@@ -136,10 +146,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios" :path: ".symlinks/plugins/integration_test/ios"
local_auth_ios: local_auth_ios:
:path: ".symlinks/plugins/local_auth_ios/ios" :path: ".symlinks/plugins/local_auth_ios/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_foundation/darwin"
pdfx: pdfx:
:path: ".symlinks/plugins/pdfx/ios" :path: ".symlinks/plugins/pdfx/ios"
permission_handler_apple: permission_handler_apple:
@@ -148,8 +160,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_ios: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_ios/ios" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios: url_launcher_ios:
@@ -160,28 +172,30 @@ SPEC CHECKSUMS:
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
edge_detection: 9bc5ee35073b5a17c0b3b679908f01017ce3062a edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 file_picker: ce3938a0df3cc1ef404671531facef740d03f920
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370 SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7 WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 51; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -321,10 +321,12 @@
}; };
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Thin Binary"; name = "Thin Binary";
outputPaths = ( outputPaths = (
@@ -335,6 +337,7 @@
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );

View File

@@ -65,5 +65,7 @@
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
</dict> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist> </plist>

View File

@@ -1,5 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
@@ -9,26 +11,40 @@ class DocumentChangedNotifier {
final Subject<DocumentModel> _updated = PublishSubject(); final Subject<DocumentModel> _updated = PublishSubject();
final Subject<DocumentModel> _deleted = PublishSubject(); final Subject<DocumentModel> _deleted = PublishSubject();
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
void notifyUpdated(DocumentModel updated) { void notifyUpdated(DocumentModel updated) {
debugPrint("Notifying updated document ${updated.id}");
_updated.add(updated); _updated.add(updated);
} }
void notifyDeleted(DocumentModel deleted) { void notifyDeleted(DocumentModel deleted) {
debugPrint("Notifying deleted document ${deleted.id}");
_deleted.add(deleted); _deleted.add(deleted);
} }
List<StreamSubscription> listen({ void subscribe(
dynamic subscriber, {
DocumentChangedCallback? onUpdated, DocumentChangedCallback? onUpdated,
DocumentChangedCallback? onDeleted, DocumentChangedCallback? onDeleted,
}) { }) {
return [ _subscribers.putIfAbsent(
_updated.listen((value) { subscriber,
onUpdated?.call(value); () => [
}), _updated.listen((value) {
_updated.listen((value) { onUpdated?.call(value);
onDeleted?.call(value); }),
}), _deleted.listen((value) {
]; onDeleted?.call(value);
}),
],
);
}
void unsubscribe(dynamic subscriber) {
_subscribers[subscriber]?.forEach((element) {
element.cancel();
});
} }
void close() { void close() {

View File

@@ -1,30 +1,30 @@
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
/// ///
/// Base repository class which all repositories should implement /// Base repository class which all repositories should implement
/// ///
abstract class BaseRepository<State extends RepositoryState, Type> abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
extends Cubit<State> with HydratedMixin { with HydratedMixin {
final State _initialState; final IndexedRepositoryState<T> _initialState;
BaseRepository(this._initialState) : super(_initialState) { BaseRepository(this._initialState) : super(_initialState) {
hydrate(); hydrate();
} }
Stream<State?> get values => Stream<IndexedRepositoryState<T>?> get values =>
BehaviorSubject.seeded(state)..addStream(super.stream); BehaviorSubject.seeded(state)..addStream(super.stream);
State? get current => state; IndexedRepositoryState<T>? get current => state;
bool get isInitialized => state.hasLoaded; bool get isInitialized => state.hasLoaded;
Future<Type> create(Type object); Future<T> create(T object);
Future<Type?> find(int id); Future<T?> find(int id);
Future<Iterable<Type>> findAll([Iterable<int>? ids]); Future<Iterable<T>> findAll([Iterable<int>? ids]);
Future<Type> update(Type object); Future<T> update(T object);
Future<int> delete(Type object); Future<int> delete(T object);
@override @override
Future<void> clear() async { Future<void> clear() async {

View File

@@ -3,10 +3,8 @@ import 'dart:async';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/repository_state.dart';
class CorrespondentRepositoryImpl class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
extends LabelRepository<Correspondent, CorrespondentRepositoryState> {
final PaperlessLabelsApi _api; final PaperlessLabelsApi _api;
CorrespondentRepositoryImpl(this._api) CorrespondentRepositoryImpl(this._api)
@@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl
@override @override
Future<Correspondent> create(Correspondent correspondent) async { Future<Correspondent> create(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent); final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created); ..putIfAbsent(created.id!, () => created);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return created; return created;
@@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl
@override @override
Future<int> delete(Correspondent correspondent) async { Future<int> delete(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent); await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == correspondent.id); ..removeWhere((k, v) => k == correspondent.id);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent.id!; return correspondent.id!;
@@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl
Future<Correspondent?> find(int id) async { Future<Correspondent?> find(int id) async {
final correspondent = await _api.getCorrespondent(id); final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) { if (correspondent != null) {
final updatedState = {...state.values}..[id] = correspondent; final updatedState = {...state.values ?? {}}..[id] = correspondent;
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent; return correspondent;
} }
@@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl
@override @override
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async { Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids); final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e))); ..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondents; return correspondents;
@@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl
@override @override
Future<Correspondent> update(Correspondent correspondent) async { Future<Correspondent> update(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent); final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.values}..update(updated.id!, (_) => updated); final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true)); emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return updated; return updated;
} }
@@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl
} }
@override @override
Map<String, dynamic> toJson(CorrespondentRepositoryState state) { Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
return state.toJson(); return state.toJson();
} }
} }

View File

@@ -1,10 +1,8 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/document_type_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class DocumentTypeRepositoryImpl class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
extends LabelRepository<DocumentType, DocumentTypeRepositoryState> {
final PaperlessLabelsApi _api; final PaperlessLabelsApi _api;
DocumentTypeRepositoryImpl(this._api) DocumentTypeRepositoryImpl(this._api)
@@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl
@override @override
Future<DocumentType> create(DocumentType documentType) async { Future<DocumentType> create(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType); final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created); ..putIfAbsent(created.id!, () => created);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return created; return created;
@@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl
@override @override
Future<int> delete(DocumentType documentType) async { Future<int> delete(DocumentType documentType) async {
await _api.deleteDocumentType(documentType); await _api.deleteDocumentType(documentType);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == documentType.id); ..removeWhere((k, v) => k == documentType.id);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType.id!; return documentType.id!;
@@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl
Future<DocumentType?> find(int id) async { Future<DocumentType?> find(int id) async {
final documentType = await _api.getDocumentType(id); final documentType = await _api.getDocumentType(id);
if (documentType != null) { if (documentType != null) {
final updatedState = {...state.values}..[id] = documentType; final updatedState = {...state.values ?? {}}..[id] = documentType;
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType; return documentType;
} }
@@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl
@override @override
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async { Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids); final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e))); ..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentTypes; return documentTypes;
@@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl
@override @override
Future<DocumentType> update(DocumentType documentType) async { Future<DocumentType> update(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType); final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.values}..update(updated.id!, (_) => updated); final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true)); emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return updated; return updated;
} }
@@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl
} }
@override @override
Map<String, dynamic> toJson(DocumentTypeRepositoryState state) { Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
return state.toJson(); return state.toJson();
} }
} }

View File

@@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override @override
Future<SavedView> create(SavedView object) async { Future<SavedView> create(SavedView object) async {
final created = await _api.save(object); final created = await _api.save(object);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created); ..putIfAbsent(created.id!, () => created);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return created; return created;
@@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override @override
Future<int> delete(SavedView view) async { Future<int> delete(SavedView view) async {
await _api.delete(view); await _api.delete(view);
final updatedState = {...state.values}..remove(view.id); final updatedState = {...state.values ?? {}}..remove(view.id);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return view.id!; return view.id!;
} }
@@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
@override @override
Future<SavedView?> find(int id) async { Future<SavedView?> find(int id) async {
final found = await _api.find(id); final found = await _api.find(id);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..update(id, (_) => found, ifAbsent: () => found); ..update(id, (_) => found, ifAbsent: () => found);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return found; return found;
@@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async { Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids); final found = await _api.findAll(ids);
final updatedState = { final updatedState = {
...state.values, ...state.values ?? {},
...{for (final view in found) view.id!: view}, ...{for (final view in found) view.id!: view},
}; };
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true)); emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
@@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
} }
@override @override
Map<String, dynamic> toJson(SavedViewRepositoryState state) { Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
return state.toJson(); return state.toJson();
} }
} }

View File

@@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject; import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class StoragePathRepositoryImpl class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
extends LabelRepository<StoragePath, StoragePathRepositoryState> {
final PaperlessLabelsApi _api; final PaperlessLabelsApi _api;
StoragePathRepositoryImpl(this._api) StoragePathRepositoryImpl(this._api)
@@ -13,7 +12,7 @@ class StoragePathRepositoryImpl
@override @override
Future<StoragePath> create(StoragePath storagePath) async { Future<StoragePath> create(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath); final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created); ..putIfAbsent(created.id!, () => created);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return created; return created;
@@ -22,7 +21,7 @@ class StoragePathRepositoryImpl
@override @override
Future<int> delete(StoragePath storagePath) async { Future<int> delete(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath); await _api.deleteStoragePath(storagePath);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == storagePath.id); ..removeWhere((k, v) => k == storagePath.id);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath.id!; return storagePath.id!;
@@ -32,7 +31,7 @@ class StoragePathRepositoryImpl
Future<StoragePath?> find(int id) async { Future<StoragePath?> find(int id) async {
final storagePath = await _api.getStoragePath(id); final storagePath = await _api.getStoragePath(id);
if (storagePath != null) { if (storagePath != null) {
final updatedState = {...state.values}..[id] = storagePath; final updatedState = {...state.values ?? {}}..[id] = storagePath;
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath; return storagePath;
} }
@@ -42,7 +41,7 @@ class StoragePathRepositoryImpl
@override @override
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async { Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids); final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e))); ..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePaths; return storagePaths;
@@ -51,7 +50,8 @@ class StoragePathRepositoryImpl
@override @override
Future<StoragePath> update(StoragePath storagePath) async { Future<StoragePath> update(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath); final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.values}..update(updated.id!, (_) => updated); final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true)); emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return updated; return updated;
} }
@@ -62,7 +62,7 @@ class StoragePathRepositoryImpl
} }
@override @override
Map<String, dynamic> toJson(StoragePathRepositoryState state) { Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
return state.toJson(); return state.toJson();
} }
} }

View File

@@ -1,10 +1,8 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/tag_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> { class TagRepositoryImpl extends LabelRepository<Tag> {
final PaperlessLabelsApi _api; final PaperlessLabelsApi _api;
TagRepositoryImpl(this._api) : super(const TagRepositoryState()); TagRepositoryImpl(this._api) : super(const TagRepositoryState());
@@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override @override
Future<Tag> create(Tag object) async { Future<Tag> create(Tag object) async {
final created = await _api.saveTag(object); final created = await _api.saveTag(object);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created); ..putIfAbsent(created.id!, () => created);
emit(TagRepositoryState(values: updatedState, hasLoaded: true)); emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return created; return created;
@@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override @override
Future<int> delete(Tag tag) async { Future<int> delete(Tag tag) async {
await _api.deleteTag(tag); await _api.deleteTag(tag);
final updatedState = {...state.values}..removeWhere((k, v) => k == tag.id); final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == tag.id);
emit(TagRepositoryState(values: updatedState, hasLoaded: true)); emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag.id!; return tag.id!;
} }
@@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
Future<Tag?> find(int id) async { Future<Tag?> find(int id) async {
final tag = await _api.getTag(id); final tag = await _api.getTag(id);
if (tag != null) { if (tag != null) {
final updatedState = {...state.values}..[id] = tag; final updatedState = {...state.values ?? {}}..[id] = tag;
emit(TagRepositoryState(values: updatedState, hasLoaded: true)); emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag; return tag;
} }
@@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override @override
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async { Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids); final tags = await _api.getTags(ids);
final updatedState = {...state.values} final updatedState = {...state.values ?? {}}
..addEntries(tags.map((e) => MapEntry(e.id!, e))); ..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(TagRepositoryState(values: updatedState, hasLoaded: true)); emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tags; return tags;
@@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
@override @override
Future<Tag> update(Tag tag) async { Future<Tag> update(Tag tag) async {
final updated = await _api.updateTag(tag); final updated = await _api.updateTag(tag);
final updatedState = {...state.values}..update(updated.id!, (_) => updated); final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(TagRepositoryState(values: updatedState, hasLoaded: true)); emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return updated; return updated;
} }
@@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
} }
@override @override
Map<String, dynamic>? toJson(TagRepositoryState state) { Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
return state.toJson(); return state.toJson();
} }
} }

View File

@@ -1,8 +1,7 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
abstract class LabelRepository<T extends Label, State extends RepositoryState> abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
extends BaseRepository<State, T> { LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
LabelRepository(State initial) : super(initial);
} }

View File

@@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget {
return MultiRepositoryProvider( return MultiRepositoryProvider(
providers: [ providers: [
RepositoryProvider( RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
), ),
RepositoryProvider( RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
), ),
RepositoryProvider( RepositoryProvider(
create: (context) => context create: (context) => context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
), ),
RepositoryProvider( RepositoryProvider(
create: (context) => create: (context) => context.read<LabelRepository<Tag>>(),
context.read<LabelRepository<Tag, TagRepositoryState>>(),
), ),
], ],
child: child, child: child,

View File

@@ -1,8 +1,8 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart'; import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart'; import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
abstract class SavedViewRepository abstract class SavedViewRepository extends BaseRepository<SavedView> {
extends BaseRepository<SavedViewRepositoryState, SavedView> {
SavedViewRepository(super.initialState); SavedViewRepository(super.initialState);
} }

View File

@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'correspondent_repository_state.g.dart'; part 'correspondent_repository_state.g.dart';
@JsonSerializable() @JsonSerializable()
class CorrespondentRepositoryState class CorrespondentRepositoryState
extends RepositoryState<Map<int, Correspondent>> { extends IndexedRepositoryState<Correspondent> {
const CorrespondentRepositoryState({ const CorrespondentRepositoryState({
super.values = const {}, super.values = const {},
super.hasLoaded, super.hasLoaded,

View File

@@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson(
Map<String, dynamic> _$CorrespondentRepositoryStateToJson( Map<String, dynamic> _$CorrespondentRepositoryStateToJson(
CorrespondentRepositoryState instance) => CorrespondentRepositoryState instance) =>
<String, dynamic>{ <String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded, 'hasLoaded': instance.hasLoaded,
}; };

View File

@@ -1,20 +1,21 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'document_type_repository_state.g.dart'; part 'document_type_repository_state.g.dart';
@JsonSerializable() @JsonSerializable()
class DocumentTypeRepositoryState class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
extends RepositoryState<Map<int, DocumentType>> {
const DocumentTypeRepositoryState({ const DocumentTypeRepositoryState({
super.values = const {}, super.values = const {},
super.hasLoaded, super.hasLoaded,
}); });
@override @override
DocumentTypeRepositoryState copyWith( DocumentTypeRepositoryState copyWith({
{Map<int, DocumentType>? values, bool? hasLoaded}) { Map<int, DocumentType>? values,
bool? hasLoaded,
}) {
return DocumentTypeRepositoryState( return DocumentTypeRepositoryState(
values: values ?? this.values, values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson(
Map<String, dynamic> _$DocumentTypeRepositoryStateToJson( Map<String, dynamic> _$DocumentTypeRepositoryStateToJson(
DocumentTypeRepositoryState instance) => DocumentTypeRepositoryState instance) =>
<String, dynamic>{ <String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded, 'hasLoaded': instance.hasLoaded,
}; };

View File

@@ -1,11 +1,11 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'saved_view_repository_state.g.dart'; part 'saved_view_repository_state.g.dart';
@JsonSerializable() @JsonSerializable()
class SavedViewRepositoryState extends RepositoryState<Map<int, SavedView>> { class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
const SavedViewRepositoryState({ const SavedViewRepositoryState({
super.values = const {}, super.values = const {},
super.hasLoaded = false, super.hasLoaded = false,

View File

@@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
Map<String, dynamic> _$SavedViewRepositoryStateToJson( Map<String, dynamic> _$SavedViewRepositoryStateToJson(
SavedViewRepositoryState instance) => SavedViewRepositoryState instance) =>
<String, dynamic>{ <String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded, 'hasLoaded': instance.hasLoaded,
}; };

View File

@@ -1,20 +1,21 @@
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'storage_path_repository_state.g.dart'; part 'storage_path_repository_state.g.dart';
@JsonSerializable() @JsonSerializable()
class StoragePathRepositoryState class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
extends RepositoryState<Map<int, StoragePath>> {
const StoragePathRepositoryState({ const StoragePathRepositoryState({
super.values = const {}, super.values = const {},
super.hasLoaded = false, super.hasLoaded = false,
}); });
@override @override
StoragePathRepositoryState copyWith( StoragePathRepositoryState copyWith({
{Map<int, StoragePath>? values, bool? hasLoaded}) { Map<int, StoragePath>? values,
bool? hasLoaded,
}) {
return StoragePathRepositoryState( return StoragePathRepositoryState(
values: values ?? this.values, values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson(
Map<String, dynamic> _$StoragePathRepositoryStateToJson( Map<String, dynamic> _$StoragePathRepositoryStateToJson(
StoragePathRepositoryState instance) => StoragePathRepositoryState instance) =>
<String, dynamic>{ <String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded, 'hasLoaded': instance.hasLoaded,
}; };

View File

@@ -1,18 +1,21 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'tag_repository_state.g.dart'; part 'tag_repository_state.g.dart';
@JsonSerializable() @JsonSerializable()
class TagRepositoryState extends RepositoryState<Map<int, Tag>> { class TagRepositoryState extends IndexedRepositoryState<Tag> {
const TagRepositoryState({ const TagRepositoryState({
super.values = const {}, super.values = const {},
super.hasLoaded = false, super.hasLoaded = false,
}); });
@override @override
TagRepositoryState copyWith({Map<int, Tag>? values, bool? hasLoaded}) { TagRepositoryState copyWith({
Map<int, Tag>? values,
bool? hasLoaded,
}) {
return TagRepositoryState( return TagRepositoryState(
values: values ?? this.values, values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded, hasLoaded: hasLoaded ?? this.hasLoaded,

View File

@@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) => Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) =>
<String, dynamic>{ <String, dynamic>{
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)), 'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
'hasLoaded': instance.hasLoaded, 'hasLoaded': instance.hasLoaded,
}; };

View File

@@ -0,0 +1,16 @@
abstract class IndexedRepositoryState<T> {
final Map<int, T>? values;
final bool hasLoaded;
const IndexedRepositoryState({
required this.values,
this.hasLoaded = false,
}) : assert(!(values == null) || !hasLoaded);
IndexedRepositoryState.loaded(this.values) : hasLoaded = true;
IndexedRepositoryState<T> copyWith({
Map<int, T>? values,
bool? hasLoaded,
});
}

View File

@@ -1,16 +0,0 @@
abstract class RepositoryState<T> {
final T values;
final bool hasLoaded;
const RepositoryState({
required this.values,
this.hasLoaded = false,
});
RepositoryState.loaded(this.values) : hasLoaded = true;
RepositoryState<T> copyWith({
T? values,
bool? hasLoaded,
});
}

View File

@@ -27,7 +27,7 @@ class GithubIssueService {
..tryPutIfAbsent('assignees', () => assignees?.join(',')) ..tryPutIfAbsent('assignees', () => assignees?.join(','))
..tryPutIfAbsent('project', () => project), ..tryPutIfAbsent('project', () => project),
); );
log("[GitHubIssueService] Creating GitHub issue: " + uri.toString()); debugPrint("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
launchUrl( launchUrl(
uri, uri,
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,

View File

@@ -4,6 +4,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Shows a full screen search page and returns the search result selected by /// Shows a full screen search page and returns the search result selected by
/// the user when the page is closed. /// the user when the page is closed.
@@ -221,12 +222,13 @@ abstract class SearchDelegate<T> {
final ColorScheme colorScheme = theme.colorScheme; final ColorScheme colorScheme = theme.colorScheme;
return theme.copyWith( return theme.copyWith(
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
brightness: colorScheme.brightness, systemOverlayStyle: colorScheme.brightness == Brightness.light
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
backgroundColor: colorScheme.brightness == Brightness.dark backgroundColor: colorScheme.brightness == Brightness.dark
? Colors.grey[900] ? Colors.grey[900]
: Colors.white, : Colors.white,
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
textTheme: theme.textTheme,
), ),
inputDecorationTheme: searchFieldDecorationTheme ?? inputDecorationTheme: searchFieldDecorationTheme ??
InputDecorationTheme( InputDecorationTheme(

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ShimmerPlaceholder extends StatelessWidget {
final Widget child;
const ShimmerPlaceholder({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: child,
);
}
}

View File

@@ -1,23 +1,32 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.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/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
part 'document_details_state.dart'; part 'document_details_state.dart';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> { class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final PaperlessDocumentsApi _api; final PaperlessDocumentsApi _api;
final DocumentChangedNotifier _notifier;
DocumentDetailsCubit(this._api, DocumentModel initialDocument) final List<StreamSubscription> _subscriptions = [];
: super(DocumentDetailsState(document: initialDocument)) { DocumentDetailsCubit(
this._api,
this._notifier, {
required DocumentModel initialDocument,
}) : super(DocumentDetailsState(document: initialDocument)) {
_notifier.subscribe(this, onUpdated: replace);
loadSuggestions(); loadSuggestions();
} }
Future<void> delete(DocumentModel document) async { Future<void> delete(DocumentModel document) async {
await _api.delete(document); await _api.delete(document);
_notifier.notifyDeleted(document);
} }
Future<void> loadSuggestions() async { Future<void> loadSuggestions() async {
@@ -41,7 +50,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final int asn = await _api.findNextAsn(); final int asn = await _api.findNextAsn();
final updatedDocument = final updatedDocument =
await _api.update(document.copyWith(archiveSerialNumber: asn)); await _api.update(document.copyWith(archiveSerialNumber: asn));
emit(state.copyWith(document: updatedDocument)); _notifier.notifyUpdated(updatedDocument);
} }
} }
@@ -60,7 +69,16 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
); );
} }
void replaceDocument(DocumentModel document) { void replace(DocumentModel document) {
emit(state.copyWith(document: document)); emit(state.copyWith(document: document));
} }
@override
Future<void> close() {
for (final element in _subscriptions) {
element.cancel();
}
_notifier.unsubscribe(this);
return super.close();
}
} }

View File

@@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:badges/badges.dart' as b;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -8,13 +8,11 @@ import 'package:intl/intl.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart'; import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/similar_documents_view.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart'; import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
@@ -30,9 +28,7 @@ import 'package:paperless_mobile/helpers/format_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:badges/badges.dart' as b; import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
//TODO: Refactor this into several widgets //TODO: Refactor this into several widgets
class DocumentDetailsPage extends StatefulWidget { class DocumentDetailsPage extends StatefulWidget {
@@ -79,16 +75,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar( SliverAppBar(
leading: IconButton( leading: const BackButton(),
icon: const Icon(
Icons.arrow_back,
color: Colors
.black, //TODO: check if there is a way to dynamically determine color...
),
onPressed: () => Navigator.of(context).pop(
context.read<DocumentDetailsCubit>().state.document,
),
),
floating: true, floating: true,
pinned: true, pinned: true,
expandedHeight: 200.0, expandedHeight: 200.0,
@@ -153,6 +140,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
builder: (context, state) { builder: (context, state) {
return BlocProvider( return BlocProvider(
create: (context) => SimilarDocumentsCubit( create: (context) => SimilarDocumentsCubit(
context.read(),
context.read(), context.read(),
documentId: state.document.id, documentId: state.document.id,
), ),
@@ -168,7 +156,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
_buildDocumentMetaDataView( _buildDocumentMetaDataView(
state.document, state.document,
), ),
_buildSimilarDocumentsView(), const SimilarDocumentsView(),
], ],
), ),
).paddedSymmetrically(horizontal: 8); ).paddedSymmetrically(horizontal: 8);
@@ -284,6 +272,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
documentTypeRepository: context.read(), documentTypeRepository: context.read(),
storagePathRepository: context.read(), storagePathRepository: context.read(),
tagRepository: context.read(), tagRepository: context.read(),
notifier: context.read(),
), ),
), ),
BlocProvider<DocumentDetailsCubit>.value( BlocProvider<DocumentDetailsCubit>.value(
@@ -294,7 +283,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.document != current.document, previous.document != current.document,
listener: (context, state) { listener: (context, state) {
cubit.replaceDocument(state.document); cubit.replace(state.document);
}, },
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { builder: (context, state) {
@@ -461,7 +450,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
visible: document.documentType != null, visible: document.documentType != null,
child: _DetailsItem( child: _DetailsItem(
label: S.of(context).documentDocumentTypePropertyLabel, label: S.of(context).documentDocumentTypePropertyLabel,
content: LabelText<DocumentType, DocumentTypeRepositoryState>( content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
id: document.documentType, id: document.documentType,
), ),
@@ -471,7 +460,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
visible: document.correspondent != null, visible: document.correspondent != null,
child: _DetailsItem( child: _DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel, label: S.of(context).documentCorrespondentPropertyLabel,
content: LabelText<Correspondent, CorrespondentRepositoryState>( content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
id: document.correspondent, id: document.correspondent,
), ),
@@ -555,10 +544,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
), ),
); );
} }
Widget _buildSimilarDocumentsView() {
return const SimilarDocumentsView();
}
} }
class _DetailsItem extends StatelessWidget { class _DetailsItem extends StatelessWidget {

View File

@@ -13,7 +13,13 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
DocumentSearchCubit(this.api, this.notifier) DocumentSearchCubit(this.api, this.notifier)
: super(const DocumentSearchState()); : super(const DocumentSearchState()) {
notifier.subscribe(
this,
onDeleted: remove,
onUpdated: replace,
);
}
Future<void> search(String query) async { Future<void> search(String query) async {
emit(state.copyWith( emit(state.copyWith(
@@ -61,6 +67,12 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
)); ));
} }
@override
Future<void> close() {
notifier.unsubscribe(this);
return super.close();
}
@override @override
DocumentSearchState? fromJson(Map<String, dynamic> json) { DocumentSearchState? fromJson(Map<String, dynamic> json) {
return DocumentSearchState.fromJson(json); return DocumentSearchState.fromJson(json);

View File

@@ -158,18 +158,16 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
isLabelClickable: false, isLabelClickable: false,
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
onTap: (document) async { enableHeroAnimation: false,
final updatedDocument = await Navigator.pushNamed( onTap: (document) {
Navigator.pushNamed(
context, context,
DocumentDetailsRoute.routeName, DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments( arguments: DocumentDetailsRouteArguments(
document: document, document: document,
isLabelClickable: false, isLabelClickable: false,
), ),
) as DocumentModel?; );
if (updatedDocument != document) {
context.read<DocumentSearchCubit>().reload();
}
}, },
) )
], ],

View File

@@ -14,21 +14,17 @@ part 'document_upload_state.dart';
class DocumentUploadCubit extends Cubit<DocumentUploadState> { class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final PaperlessDocumentsApi _documentApi; final PaperlessDocumentsApi _documentApi;
final LabelRepository<Tag, TagRepositoryState> _tagRepository; final LabelRepository<Tag> _tagRepository;
final LabelRepository<Correspondent, CorrespondentRepositoryState> final LabelRepository<Correspondent> _correspondentRepository;
_correspondentRepository; final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
_documentTypeRepository;
final List<StreamSubscription> _subs = []; final List<StreamSubscription> _subs = [];
DocumentUploadCubit({ DocumentUploadCubit({
required PaperlessDocumentsApi documentApi, required PaperlessDocumentsApi documentApi,
required LabelRepository<Tag, TagRepositoryState> tagRepository, required LabelRepository<Tag> tagRepository,
required LabelRepository<Correspondent, CorrespondentRepositoryState> required LabelRepository<Correspondent> correspondentRepository,
correspondentRepository, required LabelRepository<DocumentType> documentTypeRepository,
required LabelRepository<DocumentType, DocumentTypeRepositoryState>
documentTypeRepository,
}) : _documentApi = documentApi, }) : _documentApi = documentApi,
_tagRepository = tagRepository, _tagRepository = tagRepository,
_correspondentRepository = correspondentRepository, _correspondentRepository = correspondentRepository,

View File

@@ -8,10 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/document_type_repository_state.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.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/document_upload/cubit/document_upload_cubit.dart'; import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
@@ -20,7 +17,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/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/constants.dart';
class DocumentUploadPreparationPage extends StatefulWidget { class DocumentUploadPreparationPage extends StatefulWidget {
final Uint8List fileBytes; final Uint8List fileBytes;
@@ -173,9 +169,8 @@ class _DocumentUploadPreparationPageState
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) => labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider( RepositoryProvider(
create: (context) => context.read< create: (context) =>
LabelRepository<DocumentType, context.read<LabelRepository<DocumentType>>(),
DocumentTypeRepositoryState>>(),
child: AddDocumentTypePage(initialName: initialName), child: AddDocumentTypePage(initialName: initialName),
), ),
textFieldLabel: textFieldLabel:
@@ -189,9 +184,8 @@ class _DocumentUploadPreparationPageState
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) => labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider( RepositoryProvider(
create: (context) => context.read< create: (context) =>
LabelRepository<Correspondent, context.read<LabelRepository<Correspondent>>(),
CorrespondentRepositoryState>>(),
child: AddCorrespondentPage(initialName: initialName), child: AddCorrespondentPage(initialName: initialName),
), ),
textFieldLabel: textFieldLabel:

View File

@@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/foundation.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/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/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';
@@ -17,14 +17,21 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) { DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
reload(); notifier.subscribe(
this,
onUpdated: replace,
onDeleted: remove,
);
} }
Future<void> bulkRemove(List<DocumentModel> documents) async { Future<void> bulkDelete(List<DocumentModel> documents) async {
log("[DocumentsCubit] bulkRemove"); debugPrint("[DocumentsCubit] bulkRemove");
await api.bulkAction( await api.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)), BulkDeleteAction(documents.map((doc) => doc.id)),
); );
for (final deletedDoc in documents) {
notifier.notifyDeleted(deletedDoc);
}
await reload(); await reload();
} }
@@ -33,7 +40,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
Iterable<int> addTags = const [], Iterable<int> addTags = const [],
Iterable<int> removeTags = const [], Iterable<int> removeTags = const [],
}) async { }) async {
log("[DocumentsCubit] bulkEditTags"); debugPrint("[DocumentsCubit] bulkEditTags");
await api.bulkAction(BulkModifyTagsAction( await api.bulkAction(BulkModifyTagsAction(
documents.map((doc) => doc.id), documents.map((doc) => doc.id),
addTags: addTags, addTags: addTags,
@@ -43,7 +50,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
} }
void toggleDocumentSelection(DocumentModel model) { void toggleDocumentSelection(DocumentModel model) {
log("[DocumentsCubit] toggleSelection"); debugPrint("[DocumentsCubit] toggleSelection");
if (state.selectedIds.contains(model.id)) { if (state.selectedIds.contains(model.id)) {
emit( emit(
state.copyWith( state.copyWith(
@@ -58,12 +65,12 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
} }
void resetSelection() { void resetSelection() {
log("[DocumentsCubit] resetSelection"); debugPrint("[DocumentsCubit] resetSelection");
emit(state.copyWith(selection: [])); emit(state.copyWith(selection: []));
} }
void reset() { void reset() {
log("[DocumentsCubit] reset"); debugPrint("[DocumentsCubit] reset");
emit(const DocumentsState()); emit(const DocumentsState());
} }
@@ -81,4 +88,10 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
Map<String, dynamic>? toJson(DocumentsState state) { Map<String, dynamic>? toJson(DocumentsState state) {
return state.toJson(); return state.toJson();
} }
@override
Future<void> close() {
notifier.unsubscribe(this);
return super.close();
}
} }

View File

@@ -3,7 +3,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
class DocumentsState extends PagedDocumentsState { class DocumentsState extends PagedDocumentsState {
@JsonKey(ignore: true) @JsonKey(includeFromJson: true, includeToJson: false)
final List<DocumentModel> selection; final List<DocumentModel> selection;
const DocumentsState({ const DocumentsState({
@@ -34,11 +34,8 @@ class DocumentsState extends PagedDocumentsState {
@override @override
List<Object?> get props => [ List<Object?> get props => [
hasLoaded,
filter,
value,
selection, selection,
isLoading, ...super.props,
]; ];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {

View File

@@ -160,8 +160,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false, notAssignedSelectable: false,
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<StoragePath>>(),
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: AddStoragePathPage(initalValue: initialValue), child: AddStoragePathPage(initalValue: initialValue),
), ),
textFieldLabel: S.of(context).documentStoragePathPropertyLabel, textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
@@ -182,8 +181,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false, notAssignedSelectable: false,
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider( labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: AddCorrespondentPage(initialName: initialValue), child: AddCorrespondentPage(initialName: initialValue),
), ),
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel, textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
@@ -215,8 +213,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
notAssignedSelectable: false, notAssignedSelectable: false,
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider( labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: AddDocumentTypePage( child: AddDocumentTypePage(
initialName: currentInput, initialName: currentInput,
), ),

View File

@@ -249,7 +249,7 @@ class _DocumentsPageState extends State<DocumentsPage>
Builder( Builder(
builder: (context) { builder: (context) {
return RefreshIndicator( return RefreshIndicator(
edgeOffset: kToolbarHeight, edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadDocuments, onRefresh: _onReloadDocuments,
notificationPredicate: (_) => notificationPredicate: (_) =>
connectivityState.isConnected, connectivityState.isConnected,
@@ -263,13 +263,14 @@ class _DocumentsPageState extends State<DocumentsPage>
), ),
_buildViewActions(), _buildViewActions(),
BlocBuilder<DocumentsCubit, DocumentsState>( BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) => // Not required anymore since saved views are now handled separately
!const ListEquality().equals( // buildWhen: (previous, current) =>
previous.documents, // !const ListEquality().equals(
current.documents, // previous.documents,
) || // current.documents,
previous.selectedIds != // ) ||
current.selectedIds, // previous.selectedIds !=
// current.selectedIds,
builder: (context, state) { builder: (context, state) {
if (state.hasLoaded && if (state.hasLoaded &&
state.documents.isEmpty) { state.documents.isEmpty) {
@@ -323,7 +324,7 @@ class _DocumentsPageState extends State<DocumentsPage>
Builder( Builder(
builder: (context) { builder: (context) {
return RefreshIndicator( return RefreshIndicator(
edgeOffset: kToolbarHeight, edgeOffset: kToolbarHeight + kTextTabBarHeight,
onRefresh: _onReloadSavedViews, onRefresh: _onReloadSavedViews,
notificationPredicate: (_) => notificationPredicate: (_) =>
connectivityState.isConnected, connectivityState.isConnected,
@@ -390,7 +391,7 @@ class _DocumentsPageState extends State<DocumentsPage>
try { try {
await context await context
.read<DocumentsCubit>() .read<DocumentsCubit>()
.bulkRemove(documentsState.selection); .bulkDelete(documentsState.selection);
showSnackBar( showSnackBar(
context, context,
S.of(context).documentsPageBulkDeleteSuccessfulText, S.of(context).documentsPageBulkDeleteSuccessfulText,
@@ -467,20 +468,14 @@ class _DocumentsPageState extends State<DocumentsPage>
} }
} }
Future<void> _openDetails(DocumentModel document) async { void _openDetails(DocumentModel document) {
final updatedModel = await Navigator.pushNamed( Navigator.pushNamed(
context, context,
DocumentDetailsRoute.routeName, DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments( arguments: DocumentDetailsRouteArguments(
document: document, document: document,
), ),
) as DocumentModel?; );
// final updatedModel = await Navigator.of(context).push<DocumentModel?>(
// _buildDetailsPageRoute(document),
// );
if (updatedModel != document) {
context.read<DocumentsCubit>().reload();
}
} }
void _addTagToFilter(int tagId) { void _addTagToFilter(int tagId) {

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_grid_loading_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -23,6 +23,7 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
final void Function(int? id)? onDocumentTypeSelected; final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected; final void Function(int? id)? onStoragePathSelected;
bool get showLoadingPlaceholder => (!hasLoaded && isLoading);
const AdaptiveDocumentsView({ const AdaptiveDocumentsView({
super.key, super.key,
this.selectedDocumentIds = const [], this.selectedDocumentIds = const [],
@@ -56,6 +57,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
super.onTap, super.onTap,
super.selectedDocumentIds, super.selectedDocumentIds,
super.viewType, super.viewType,
super.enableHeroAnimation,
required super.isLoading, required super.isLoading,
required super.hasLoaded, required super.hasLoaded,
}); });
@@ -71,8 +73,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
} }
Widget _buildListView() { Widget _buildListView() {
if (!hasLoaded && isLoading) { if (showLoadingPlaceholder) {
return const DocumentsListLoadingWidget(); return DocumentsListLoadingWidget.sliver();
} }
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
@@ -91,6 +93,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onCorrespondentSelected: onCorrespondentSelected, onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected, onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected, onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
), ),
); );
}, },
@@ -99,8 +102,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
} }
Widget _buildGridView() { Widget _buildGridView() {
if (!hasLoaded && isLoading) { if (showLoadingPlaceholder) {
return const DocumentsListLoadingWidget(); return DocumentGridLoadingWidget.sliver();
} }
return SliverGrid.builder( return SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
@@ -162,10 +165,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
} }
Widget _buildListView() { Widget _buildListView() {
if (!hasLoaded && isLoading) { if (showLoadingPlaceholder) {
return const CustomScrollView(slivers: [ return DocumentsListLoadingWidget();
DocumentsListLoadingWidget(),
]);
} }
return ListView.builder( return ListView.builder(
@@ -194,12 +195,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
} }
Widget _buildGridView() { Widget _buildGridView() {
if (!hasLoaded && isLoading) { if (showLoadingPlaceholder) {
return const CustomScrollView( return DocumentGridLoadingWidget();
slivers: [
DocumentsListLoadingWidget(),
],
); //TODO: Build grid skeleton
} }
return GridView.builder( return GridView.builder(
controller: scrollController, controller: scrollController,

View File

@@ -0,0 +1,102 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
import 'package:shimmer/shimmer.dart';
class DocumentGridLoadingWidget extends StatelessWidget
with DocumentItemPlaceholder {
final bool _isSliver;
@override
final Random random = Random(1257195195);
DocumentGridLoadingWidget({super.key}) : _isSliver = false;
DocumentGridLoadingWidget.sliver({super.key}) : _isSliver = true;
@override
Widget build(BuildContext context) {
const delegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
);
if (_isSliver) {
return SliverGrid.builder(
gridDelegate: delegate,
itemBuilder: (context, index) => _buildPlaceholderGridItem(context),
);
}
return GridView.builder(
gridDelegate: delegate,
itemBuilder: (context, index) => _buildPlaceholderGridItem(context),
);
}
Widget _buildPlaceholderGridItem(BuildContext context) {
final values = nextValues;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerPlaceholder(
child: AspectRatio(
aspectRatio: 1,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
color: Colors.white,
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ShimmerPlaceholder(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextPlaceholder(
length: values.correspondentLength,
fontSize: 16,
).padded(1),
TextPlaceholder(
length: values.titleLength,
fontSize: 16,
),
if (values.tagCount > 0) ...[
const Spacer(),
TagsPlaceholder(
count: values.tagCount,
dense: true,
),
],
const Spacer(),
TextPlaceholder(
length: 100,
fontSize:
Theme.of(context).textTheme.bodySmall!.fontSize!,
),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -1,42 +1,42 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:shimmer/shimmer.dart'; import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_item_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
class DocumentsListLoadingWidget extends StatelessWidget { class DocumentsListLoadingWidget extends StatelessWidget
static const _tags = [" ", " ", " "]; with DocumentItemPlaceholder {
static const _titleLengths = <double>[double.infinity, 150.0, 200.0]; final bool _isSliver;
static const _correspondentLengths = <double>[200.0, 300.0, 150.0]; DocumentsListLoadingWidget({super.key}) : _isSliver = false;
static const _fontSize = 16.0;
const DocumentsListLoadingWidget({super.key DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
});
@override
final Random random = Random(1209571050);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _random = Random(); if (_isSliver) {
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) => _buildFakeListItem(context),
return _buildFakeListItem(context, _random); ),
}, );
), } else {
); return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildFakeListItem(context),
);
}
} }
Widget _buildFakeListItem(BuildContext context, Random random) { Widget _buildFakeListItem(BuildContext context) {
final tagCount = random.nextInt(_tags.length + 1); const fontSize = 14.0;
final correspondentLength = final values = nextValues;
_correspondentLengths[random.nextInt(_correspondentLengths.length - 1)]; return ShimmerPlaceholder(
final titleLength = _titleLengths[random.nextInt(_titleLengths.length - 1)];
return Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.all(8), contentPadding: const EdgeInsets.all(8),
dense: true, dense: true,
@@ -45,15 +45,17 @@ class DocumentsListLoadingWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
color: Colors.white, color: Colors.white,
height: 50, height: double.infinity,
width: 35, width: 35,
), ),
), ),
title: Container( title: Row(
padding: const EdgeInsets.symmetric(vertical: 2.0), children: [
width: correspondentLength, TextPlaceholder(
height: _fontSize, length: values.correspondentLength,
color: Colors.white, fontSize: fontSize,
),
],
), ),
subtitle: Padding( subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0), padding: const EdgeInsets.symmetric(vertical: 2.0),
@@ -61,21 +63,16 @@ class DocumentsListLoadingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Container( TextPlaceholder(
padding: const EdgeInsets.symmetric(vertical: 2.0), length: values.titleLength,
height: _fontSize, fontSize: fontSize,
width: titleLength, ),
color: Colors.white, if (values.tagCount > 0)
TagsPlaceholder(count: values.tagCount, dense: true),
TextPlaceholder(
length: 100,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize!,
), ),
Wrap(
spacing: 2.0,
children: List.generate(
tagCount,
(index) => InputChip(
label: Text(_tags[random.nextInt(_tags.length)]),
),
),
).paddedOnly(top: 4),
], ],
), ),
), ),

View File

@@ -56,7 +56,7 @@ class DocumentListItem extends DocumentItem {
Text( Text(
document.title, document.title,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: document.tags.isEmpty ? 2 : 1, maxLines: 1,
), ),
AbsorbPointer( AbsorbPointer(
absorbing: isSelectionActive, absorbing: isSelectionActive,

View File

@@ -0,0 +1,30 @@
import 'dart:math';
mixin DocumentItemPlaceholder {
static const _tags = [" ", " ", " "];
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
static const _correspondentLengths = <double>[120.0, 80.0, 40.0];
Random get random;
RandomDocumentItemPlaceholderValues get nextValues {
return RandomDocumentItemPlaceholderValues(
tagCount: random.nextInt(_tags.length + 1),
correspondentLength: _correspondentLengths[
random.nextInt(_correspondentLengths.length - 1)],
titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)],
);
}
}
class RandomDocumentItemPlaceholderValues {
final int tagCount;
final double correspondentLength;
final double titleLength;
RandomDocumentItemPlaceholderValues({
required this.tagCount,
required this.correspondentLength,
required this.titleLength,
});
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class TagsPlaceholder extends StatelessWidget {
static const _lengths = [24, 36, 16, 48];
final int count;
final bool dense;
const TagsPlaceholder({
super.key,
required this.count,
required this.dense,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: ListView.separated(
itemCount: count,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => FilterChip(
labelPadding:
dense ? const EdgeInsets.symmetric(horizontal: 2) : null,
padding: dense ? const EdgeInsets.all(4) : null,
visualDensity: const VisualDensity(vertical: -2),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide.none,
onSelected: (_) {},
selected: false,
label: Text(
List.filled(_lengths[index], " ").join(),
),
),
separatorBuilder: (context, _) => const SizedBox(width: 4),
),
);
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class TextPlaceholder extends StatelessWidget {
final double length;
final double fontSize;
const TextPlaceholder({
super.key,
required this.length,
required this.fontSize,
});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
width: length,
height: fontSize,
);
}
}

View File

@@ -44,16 +44,12 @@ class SortDocumentsButton extends StatelessWidget {
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => LabelCubit<DocumentType>( create: (context) => LabelCubit<DocumentType>(
context.read< context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType,
DocumentTypeRepositoryState>>(),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => LabelCubit<Correspondent>( create: (context) => LabelCubit<Correspondent>(
context.read< context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent,
CorrespondentRepositoryState>>(),
), ),
), ),
], ],

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.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:collection/collection.dart'; import 'package:collection/collection.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';
@@ -16,31 +17,28 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
final DocumentModel _initialDocument; final DocumentModel _initialDocument;
final PaperlessDocumentsApi _docsApi; final PaperlessDocumentsApi _docsApi;
final LabelRepository<Correspondent, CorrespondentRepositoryState> final DocumentChangedNotifier _notifier;
_correspondentRepository; final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType, DocumentTypeRepositoryState> final LabelRepository<DocumentType> _documentTypeRepository;
_documentTypeRepository; final LabelRepository<StoragePath> _storagePathRepository;
final LabelRepository<StoragePath, StoragePathRepositoryState> final LabelRepository<Tag> _tagRepository;
_storagePathRepository;
final LabelRepository<Tag, TagRepositoryState> _tagRepository;
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
EditDocumentCubit( EditDocumentCubit(
DocumentModel document, { DocumentModel document, {
required PaperlessDocumentsApi documentsApi, required PaperlessDocumentsApi documentsApi,
required LabelRepository<Correspondent, CorrespondentRepositoryState> required LabelRepository<Correspondent> correspondentRepository,
correspondentRepository, required LabelRepository<DocumentType> documentTypeRepository,
required LabelRepository<DocumentType, DocumentTypeRepositoryState> required LabelRepository<StoragePath> storagePathRepository,
documentTypeRepository, required LabelRepository<Tag> tagRepository,
required LabelRepository<StoragePath, StoragePathRepositoryState> required DocumentChangedNotifier notifier,
storagePathRepository,
required LabelRepository<Tag, TagRepositoryState> tagRepository,
}) : _initialDocument = document, }) : _initialDocument = document,
_docsApi = documentsApi, _docsApi = documentsApi,
_correspondentRepository = correspondentRepository, _correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository, _documentTypeRepository = documentTypeRepository,
_storagePathRepository = storagePathRepository, _storagePathRepository = storagePathRepository,
_tagRepository = tagRepository, _tagRepository = tagRepository,
_notifier = notifier,
super( super(
EditDocumentState( EditDocumentState(
document: document, document: document,
@@ -50,6 +48,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
tags: tagRepository.current?.values ?? {}, tags: tagRepository.current?.values ?? {},
), ),
) { ) {
_notifier.subscribe(this, onUpdated: replace);
_subscriptions.add( _subscriptions.add(
_correspondentRepository.values _correspondentRepository.values
.listen((v) => emit(state.copyWith(correspondents: v?.values))), .listen((v) => emit(state.copyWith(correspondents: v?.values))),
@@ -71,6 +70,8 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
Future<void> updateDocument(DocumentModel document) async { Future<void> updateDocument(DocumentModel document) async {
final updated = await _docsApi.update(document); final updated = await _docsApi.update(document);
_notifier.notifyUpdated(updated);
// Reload changed labels (documentCount property changes with removal/add) // Reload changed labels (documentCount property changes with removal/add)
if (document.documentType != _initialDocument.documentType) { if (document.documentType != _initialDocument.documentType) {
_documentTypeRepository _documentTypeRepository
@@ -88,7 +89,10 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
.equals(document.tags, _initialDocument.tags)) { .equals(document.tags, _initialDocument.tags)) {
_tagRepository.findAll(document.tags); _tagRepository.findAll(document.tags);
} }
emit(state.copyWith(document: updated)); }
void replace(DocumentModel document) {
emit(state.copyWith(document: document));
} }
@override @override
@@ -96,6 +100,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
for (final sub in _subscriptions) { for (final sub in _subscriptions) {
sub.cancel(); sub.cancel();
} }
_notifier.unsubscribe(this);
return super.close(); return super.close();
} }
} }

View File

@@ -3,15 +3,15 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.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/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart';
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> { class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
final LabelRepository<T, RepositoryState<Map<int, T>>> _repository; final LabelRepository<T> _repository;
StreamSubscription? _subscription; StreamSubscription? _subscription;
EditLabelCubit(LabelRepository<T, RepositoryState<Map<int, T>>> repository) EditLabelCubit(LabelRepository<T> repository)
: _repository = repository, : _repository = repository,
super(const EditLabelInitial()) { super(const EditLabelInitial()) {
_subscription = repository.values.listen( _subscription = repository.values.listen(

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.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/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -25,8 +25,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => EditLabelCubit(
context context.read<LabelRepository<T>>(),
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
), ),
child: AddLabelFormWidget( child: AddLabelFormWidget(
pageTitle: pageTitle, pageTitle: pageTitle,

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.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/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart'; import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart'; import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart'; import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -28,8 +28,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit( create: (context) => EditLabelCubit(
context context.read<LabelRepository<T>>(),
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
), ),
child: EditLabelForm( child: EditLabelForm(
label: label, label: label,

View File

@@ -15,8 +15,7 @@ class AddCorrespondentPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>( create: (context) => EditLabelCubit<Correspondent>(
context.read< context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
), ),
child: AddLabelPage<Correspondent>( child: AddLabelPage<Correspondent>(
pageTitle: Text(S.of(context).addCorrespondentPageTitle), pageTitle: Text(S.of(context).addCorrespondentPageTitle),

View File

@@ -18,8 +18,7 @@ class AddDocumentTypePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<DocumentType>( create: (context) => EditLabelCubit<DocumentType>(
context context.read<LabelRepository<DocumentType>>(),
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
), ),
child: AddLabelPage<DocumentType>( child: AddLabelPage<DocumentType>(
pageTitle: Text(S.of(context).addDocumentTypePageTitle), pageTitle: Text(S.of(context).addDocumentTypePageTitle),

View File

@@ -16,8 +16,7 @@ class AddStoragePathPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<StoragePath>( create: (context) => EditLabelCubit<StoragePath>(
context context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
), ),
child: AddLabelPage<StoragePath>( child: AddLabelPage<StoragePath>(
pageTitle: Text(S.of(context).addStoragePathPageTitle), pageTitle: Text(S.of(context).addStoragePathPageTitle),

View File

@@ -19,7 +19,7 @@ class AddTagPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<Tag>( create: (context) => EditLabelCubit<Tag>(
context.read<LabelRepository<Tag, TagRepositoryState>>(), context.read<LabelRepository<Tag>>(),
), ),
child: AddLabelPage<Tag>( child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context).addTagPageTitle), pageTitle: Text(S.of(context).addTagPageTitle),

View File

@@ -14,8 +14,7 @@ class EditCorrespondentPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>( create: (context) => EditLabelCubit<Correspondent>(
context.read< context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
), ),
child: EditLabelPage<Correspondent>( child: EditLabelPage<Correspondent>(
label: correspondent, label: correspondent,

View File

@@ -14,8 +14,7 @@ class EditDocumentTypePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<DocumentType>( create: (context) => EditLabelCubit<DocumentType>(
context context.read<LabelRepository<DocumentType>>(),
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
), ),
child: EditLabelPage<DocumentType>( child: EditLabelPage<DocumentType>(
label: documentType, label: documentType,

View File

@@ -15,8 +15,7 @@ class EditStoragePathPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<StoragePath>( create: (context) => EditLabelCubit<StoragePath>(
context context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
), ),
child: EditLabelPage<StoragePath>( child: EditLabelPage<StoragePath>(
label: storagePath, label: storagePath,

View File

@@ -18,7 +18,7 @@ class EditTagPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => EditLabelCubit<Tag>( create: (context) => EditLabelCubit<Tag>(
context.read<LabelRepository<Tag, TagRepositoryState>>(), context.read<LabelRepository<Tag>>(),
), ),
child: EditLabelPage<Tag>( child: EditLabelPage<Tag>(
label: tag, label: tag,

View File

@@ -23,6 +23,7 @@ import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart'; import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.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';
@@ -59,6 +60,7 @@ class _HomePageState extends State<HomePage> {
context.read(), context.read(),
context.read(), context.read(),
context.read(), context.read(),
context.read(),
); );
context.read<ConnectivityCubit>().reload(); context.read<ConnectivityCubit>().reload();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
@@ -228,7 +230,23 @@ class _HomePageState extends State<HomePage> {
value: _scannerCubit, value: _scannerCubit,
child: const ScannerPage(), child: const ScannerPage(),
), ),
const LabelsPage(), MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit<Correspondent>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<DocumentType>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<Tag>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<StoragePath>(context.read()),
),
],
child: const LabelsPage(),
),
BlocProvider.value( BlocProvider.value(
value: _inboxCubit, value: _inboxCubit,
child: const InboxPage(), child: const InboxPage(),
@@ -302,16 +320,10 @@ class _HomePageState extends State<HomePage> {
void _initializeData(BuildContext context) { void _initializeData(BuildContext context) {
try { try {
context.read<LabelRepository<Tag, TagRepositoryState>>().findAll(); context.read<LabelRepository<Tag>>().findAll();
context context.read<LabelRepository<Correspondent>>().findAll();
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>() context.read<LabelRepository<DocumentType>>().findAll();
.findAll(); context.read<LabelRepository<StoragePath>>().findAll();
context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
.findAll();
context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
.findAll();
context.read<SavedViewRepository>().findAll(); context.read<SavedViewRepository>().findAll();
context.read<PaperlessServerInformationCubit>().updateInformtion(); context.read<PaperlessServerInformationCubit>().updateInformtion();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {

View File

@@ -70,16 +70,10 @@ class VerifyIdentityPage extends StatelessWidget {
void _logout(BuildContext context) { void _logout(BuildContext context) {
context.read<AuthenticationCubit>().logout(); context.read<AuthenticationCubit>().logout();
context.read<LabelRepository<Tag, TagRepositoryState>>().clear(); context.read<LabelRepository<Tag>>().clear();
context context.read<LabelRepository<Correspondent>>().clear();
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>() context.read<LabelRepository<DocumentType>>().clear();
.clear(); context.read<LabelRepository<StoragePath>>().clear();
context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
.clear();
context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
.clear();
context.read<SavedViewRepository>().clear(); context.read<SavedViewRepository>().clear();
HydratedBloc.storage.clear(); HydratedBloc.storage.clear();
} }

View File

@@ -1,23 +1,20 @@
import 'dart:async'; import 'dart:async';
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/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/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart'; import 'package:paperless_mobile/features/inbox/bloc/state/inbox_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';
class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin { class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
final LabelRepository<Tag, TagRepositoryState> _tagsRepository; final LabelRepository<Tag> _tagsRepository;
final LabelRepository<Correspondent, CorrespondentRepositoryState> final LabelRepository<Correspondent> _correspondentRepository;
_correspondentRepository; final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
_documentTypeRepository;
final PaperlessDocumentsApi _documentsApi; final PaperlessDocumentsApi _documentsApi;
@override @override
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
@@ -28,7 +25,6 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
@override @override
PaperlessDocumentsApi get api => _documentsApi; PaperlessDocumentsApi get api => _documentsApi;
Timer? _taskTimer;
InboxCubit( InboxCubit(
this._tagsRepository, this._tagsRepository,
this._documentsApi, this._documentsApi,
@@ -45,11 +41,20 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
availableTags: _tagsRepository.current?.values ?? {}, availableTags: _tagsRepository.current?.values ?? {},
), ),
) { ) {
_subscriptions.addAll( notifier.subscribe(
notifier.listen( this,
onDeleted: remove, onDeleted: remove,
onUpdated: replace, onUpdated: (document) {
), if (document.tags
.toSet()
.intersection(state.inboxTags.toSet())
.isEmpty) {
remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
} else {
replace(document);
}
},
); );
_subscriptions.add( _subscriptions.add(
_tagsRepository.values.listen((event) { _tagsRepository.values.listen((event) {
@@ -74,21 +79,35 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
} }
}), }),
); );
//TODO: Do this properly in a background task.
_taskTimer = Timer.periodic(const Duration(seconds: 5), (timer) { refreshItemsInInboxCount(false);
loadInbox();
Timer.periodic(const Duration(seconds: 5), (timer) {
if (isClosed) {
timer.cancel();
}
refreshItemsInInboxCount(); refreshItemsInInboxCount();
}); });
} }
void refreshItemsInInboxCount() async { void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
final stats = await _statsApi.getServerStatistics(); final stats = await _statsApi.getServerStatistics();
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
loadInbox();
}
emit(
state.copyWith(
itemsInInboxCount: stats.documentsInInbox,
),
);
} }
/// ///
/// Fetches inbox tag ids and loads the inbox items (documents). /// Fetches inbox tag ids and loads the inbox items (documents).
/// ///
Future<void> initializeInbox() async { Future<void> loadInbox() async {
final inboxTags = await _tagsRepository.findAll().then( final inboxTags = await _tagsRepository.findAll().then(
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!), (tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
); );
@@ -104,7 +123,7 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
); );
} }
emit(state.copyWith(inboxTags: inboxTags)); emit(state.copyWith(inboxTags: inboxTags));
return updateFilter( updateFilter(
filter: DocumentFilter( filter: DocumentFilter(
sortField: SortField.added, sortField: SortField.added,
tags: IdsTagsQuery.fromIds(inboxTags), tags: IdsTagsQuery.fromIds(inboxTags),
@@ -121,11 +140,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
document.tags.toSet().intersection(state.inboxTags.toSet()); document.tags.toSet().intersection(state.inboxTags.toSet());
final updatedTags = {...document.tags}..removeAll(tagsToRemove); final updatedTags = {...document.tags}..removeAll(tagsToRemove);
await api.update( final updatedDocument = await api.update(
document.copyWith(tags: updatedTags), document.copyWith(tags: updatedTags),
); );
await remove(document); // Remove first so document is not replaced first.
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); remove(document);
notifier.notifyUpdated(updatedDocument);
return tagsToRemove; return tagsToRemove;
} }
@@ -136,10 +156,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
DocumentModel document, DocumentModel document,
Iterable<int> removedTags, Iterable<int> removedTags,
) async { ) async {
final updatedDoc = document.copyWith( final updatedDocument = await _documentsApi.update(
tags: {...document.tags, ...removedTags}, document.copyWith(
tags: {...document.tags, ...removedTags},
),
); );
await _documentsApi.update(updatedDoc); notifier.notifyUpdated(updatedDocument);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1)); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
return reload(); return reload();
} }
@@ -166,22 +188,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
} }
} }
void replaceUpdatedDocument(DocumentModel document) {
if (document.tags.any((id) => state.inboxTags.contains(id))) {
// If replaced document still has inbox tag assigned:
replace(document);
} else {
// Remove document from inbox.
remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
}
}
Future<void> assignAsn(DocumentModel document) async { Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) { if (document.archiveSerialNumber == null) {
final int asn = await _documentsApi.findNextAsn(); final int asn = await _documentsApi.findNextAsn();
final updatedDocument = await _documentsApi final updatedDocument = await _documentsApi
.update(document.copyWith(archiveSerialNumber: asn)); .update(document.copyWith(archiveSerialNumber: asn));
replace(updatedDocument); replace(updatedDocument);
} }
} }
@@ -202,7 +214,6 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
@override @override
Future<void> close() { Future<void> close() {
_taskTimer?.cancel();
for (var sub in _subscriptions) { for (var sub in _subscriptions) {
sub.cancel(); sub.cancel();
} }

View File

@@ -4,9 +4,7 @@ import 'package:paperless_mobile/features/paged_document_view/model/paged_docume
part 'inbox_state.g.dart'; part 'inbox_state.g.dart';
@JsonSerializable( @JsonSerializable(ignoreUnannotated: true)
ignoreUnannotated: true,
)
class InboxState extends PagedDocumentsState { class InboxState extends PagedDocumentsState {
final Iterable<int> inboxTags; final Iterable<int> inboxTags;

View File

@@ -31,7 +31,7 @@ class _InboxPageState extends State<InboxPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
context.read<InboxCubit>().initializeInbox(); context.read<InboxCubit>().loadInbox();
_scrollController.addListener(_listenForLoadNewData); _scrollController.addListener(_listenForLoadNewData);
} }
@@ -57,6 +57,12 @@ class _InboxPageState extends State<InboxPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final safeAreaPadding = MediaQuery.of(context).padding;
final availableHeight = MediaQuery.of(context).size.height -
kToolbarHeight -
kBottomNavigationBarHeight -
safeAreaPadding.top -
safeAreaPadding.bottom;
return Scaffold( return Scaffold(
drawer: const AppDrawer(), drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
@@ -76,97 +82,105 @@ class _InboxPageState extends State<InboxPage> {
); );
}, },
), ),
body: RefreshIndicator( body: BlocBuilder<InboxCubit, InboxState>(
edgeOffset: 78, builder: (context, state) {
onRefresh: () => context.read<InboxCubit>().initializeInbox(), return SafeArea(
child: NestedScrollView( top: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [ child: Builder(
SearchAppBar( builder: (context) {
hintText: S.of(context).documentSearchSearchDocuments, // Build a list of slivers alternating between SliverToBoxAdapter
onOpenSearch: showDocumentSearchPage, // (group header) and a SliverList (inbox items).
), final List<Widget> slivers = _groupByDate(state.documents)
], .entries
body: BlocBuilder<InboxCubit, InboxState>( .map(
builder: (context, state) { (entry) => [
if (!state.hasLoaded) { SliverToBoxAdapter(
return const CustomScrollView( child: Align(
physics: NeverScrollableScrollPhysics(), alignment: Alignment.centerLeft,
slivers: [DocumentsListLoadingWidget()], child: ClipRRect(
); borderRadius: BorderRadius.circular(32.0),
} child: Text(
entry.key,
if (state.documents.isEmpty) { style: Theme.of(context).textTheme.bodySmall,
return InboxEmptyWidget( textAlign: TextAlign.center,
emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey, ).padded(),
); ),
} ).paddedOnly(top: 8.0),
// Build a list of slivers alternating between SliverToBoxAdapter
// (group header) and a SliverList (inbox items).
final List<Widget> slivers = _groupByDate(state.documents)
.entries
.map(
(entry) => [
SliverToBoxAdapter(
child: Align(
alignment: Alignment.centerLeft,
child: ClipRRect(
borderRadius: BorderRadius.circular(32.0),
child: Text(
entry.key,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
).padded(),
),
).paddedOnly(top: 8.0),
),
SliverList(
delegate: SliverChildBuilderDelegate(
childCount: entry.value.length,
(context, index) {
if (index < entry.value.length - 1) {
return Column(
children: [
_buildListItem(
entry.value[index],
),
const Divider(
indent: 16,
endIndent: 16,
),
],
);
}
return _buildListItem(
entry.value[index],
);
},
), ),
), SliverList(
], delegate: SliverChildBuilderDelegate(
) childCount: entry.value.length,
.flattened (context, index) {
.toList() if (index < entry.value.length - 1) {
..add(const SliverToBoxAdapter(child: SizedBox(height: 78))); return Column(
// edgeOffset: kToolbarHeight, children: [
_buildListItem(
entry.value[index],
),
const Divider(
indent: 16,
endIndent: 16,
),
],
);
}
return _buildListItem(
entry.value[index],
);
},
),
),
],
)
.flattened
.toList()
..add(const SliverToBoxAdapter(child: SizedBox(height: 78)));
// edgeOffset: kToolbarHeight,
return CustomScrollView( return RefreshIndicator(
controller: _scrollController, edgeOffset: kToolbarHeight,
slivers: [ onRefresh: context.read<InboxCubit>().reload,
SliverToBoxAdapter( child: CustomScrollView(
child: HintCard( physics: state.documents.isEmpty
show: !state.isHintAcknowledged, ? const NeverScrollableScrollPhysics()
hintText: S.of(context).inboxPageUsageHintText, : const AlwaysScrollableScrollPhysics(),
onHintAcknowledged: () => controller: _scrollController,
context.read<InboxCubit>().acknowledgeHint(), slivers: [
), SearchAppBar(
hintText: S.of(context).documentSearchSearchDocuments,
onOpenSearch: showDocumentSearchPage,
),
if (state.documents.isEmpty)
SliverToBoxAdapter(
child: SizedBox(
height: availableHeight,
child: Center(
child: InboxEmptyWidget(
emptyStateRefreshIndicatorKey:
_emptyStateRefreshIndicatorKey,
),
),
),
)
else if (!state.hasLoaded)
DocumentsListLoadingWidget()
else
SliverToBoxAdapter(
child: HintCard(
show: !state.isHintAcknowledged,
hintText: S.of(context).inboxPageUsageHintText,
onHintAcknowledged: () =>
context.read<InboxCubit>().acknowledgeHint(),
),
),
...slivers,
],
), ),
...slivers, );
], },
); ),
}, );
), },
),
), ),
); );
} }
@@ -191,12 +205,7 @@ class _InboxPageState extends State<InboxPage> {
).padded(), ).padded(),
confirmDismiss: (_) => _onItemDismissed(doc), confirmDismiss: (_) => _onItemDismissed(doc),
key: UniqueKey(), key: UniqueKey(),
child: InboxItem( child: InboxItem(document: doc),
document: doc,
onDocumentUpdated: (document) {
context.read<InboxCubit>().replaceUpdatedDocument(document);
},
),
); );
} }

View File

@@ -16,7 +16,7 @@ class InboxEmptyWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
key: _emptyStateRefreshIndicatorKey, key: _emptyStateRefreshIndicatorKey,
onRefresh: () => context.read<InboxCubit>().initializeInbox(), onRefresh: () => context.read<InboxCubit>().loadInbox(),
child: Center( child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View File

@@ -1,13 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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/repository/provider/label_repositories_provider.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/workarounds/colored_chip.dart'; import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.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/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
@@ -19,12 +14,10 @@ import 'package:paperless_mobile/routes/document_details_route.dart';
class InboxItem extends StatefulWidget { class InboxItem extends StatefulWidget {
static const _a4AspectRatio = 1 / 1.4142; static const _a4AspectRatio = 1 / 1.4142;
final void Function(DocumentModel model) onDocumentUpdated;
final DocumentModel document; final DocumentModel document;
const InboxItem({ const InboxItem({
super.key, super.key,
required this.document, required this.document,
required this.onDocumentUpdated,
}); });
@override @override
@@ -41,17 +34,14 @@ class _InboxItemState extends State<InboxItem> {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () async { onTap: () async {
final updatedDocument = await Navigator.pushNamed( Navigator.pushNamed(
context, context,
DocumentDetailsRoute.routeName, DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments( arguments: DocumentDetailsRouteArguments(
document: widget.document, document: widget.document,
isLabelClickable: false, isLabelClickable: false,
), ),
) as DocumentModel?; );
if (updatedDocument != null) {
widget.onDocumentUpdated(updatedDocument);
}
}, },
child: SizedBox( child: SizedBox(
height: 200, height: 200,
@@ -104,12 +94,12 @@ class _InboxItemState extends State<InboxItem> {
); );
final actions = [ final actions = [
_buildAssignAsnAction(chipShape, context), _buildAssignAsnAction(chipShape, context),
const SizedBox(width: 4.0), const SizedBox(width: 8.0),
ColoredChipWrapper( ColoredChipWrapper(
child: ActionChip( child: ActionChip(
avatar: const Icon(Icons.delete_outline), avatar: const Icon(Icons.delete_outline),
shape: chipShape, shape: chipShape,
label: const Text("Delete document"), label: Text(S.of(context).inboxActionDeleteDocument),
onPressed: () async { onPressed: () async {
final shouldDelete = await showDialog<bool>( final shouldDelete = await showDialog<bool>(
context: context, context: context,
@@ -124,6 +114,7 @@ class _InboxItemState extends State<InboxItem> {
), ),
), ),
]; ];
// return FutureBuilder<FieldSuggestions>( // return FutureBuilder<FieldSuggestions>(
// future: _fieldSuggestions, // future: _fieldSuggestions,
// builder: (context, snapshot) { // builder: (context, snapshot) {
@@ -151,12 +142,14 @@ class _InboxItemState extends State<InboxItem> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.bolt_outlined), const Icon(Icons.bolt_outlined),
SizedBox( ConstrainedBox(
width: 40, constraints: const BoxConstraints(
maxWidth: 50,
),
child: Text( child: Text(
S.of(context).inboxPageQuickActionsLabel, S.of(context).inboxPageQuickActionsLabel,
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 2, maxLines: 3,
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
), ),
@@ -199,7 +192,7 @@ class _InboxItemState extends State<InboxItem> {
? Text( ? Text(
'${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}', '${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}',
) )
: const Text("Assign ASN"), : Text(S.of(context).inboxActionAssignAsn),
onPressed: !hasAsn onPressed: !hasAsn
? () { ? () {
setState(() { setState(() {
@@ -233,7 +226,7 @@ class _InboxItemState extends State<InboxItem> {
Icons.description_outlined, Icons.description_outlined,
size: Theme.of(context).textTheme.bodyMedium?.fontSize, size: Theme.of(context).textTheme.bodyMedium?.fontSize,
), ),
LabelText<DocumentType, DocumentTypeRepositoryState>( LabelText<DocumentType>(
id: widget.document.documentType, id: widget.document.documentType,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-", placeholder: "-",
@@ -247,7 +240,7 @@ class _InboxItemState extends State<InboxItem> {
Icons.person_outline, Icons.person_outline,
size: Theme.of(context).textTheme.bodyMedium?.fontSize, size: Theme.of(context).textTheme.bodyMedium?.fontSize,
), ),
LabelText<Correspondent, CorrespondentRepositoryState>( LabelText<Correspondent>(
id: widget.document.correspondent, id: widget.document.correspondent,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-", placeholder: "-",

View File

@@ -3,15 +3,14 @@ import 'dart:async';
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/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
class LabelCubit<T extends Label> extends Cubit<LabelState<T>> { class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final LabelRepository<T, RepositoryState> _repository; final LabelRepository<T> _repository;
late StreamSubscription _subscription; late StreamSubscription _subscription;
LabelCubit(LabelRepository<T, RepositoryState> repository) LabelCubit(LabelRepository<T> repository)
: _repository = repository, : _repository = repository,
super(LabelState( super(LabelState(
isLoaded: repository.isInitialized, isLoaded: repository.isInitialized,
@@ -22,7 +21,8 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
if (event == null) { if (event == null) {
emit(LabelState()); emit(LabelState());
} }
emit(LabelState(isLoaded: true, labels: event!.values)); emit(
LabelState(isLoaded: event!.hasLoaded, labels: event.values ?? {}));
}, },
); );
} }

View File

@@ -7,14 +7,16 @@ import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class CorrespondentBlocProvider extends StatelessWidget { class CorrespondentBlocProvider extends StatelessWidget {
final Widget child; final Widget child;
const CorrespondentBlocProvider({super.key, required this.child}); const CorrespondentBlocProvider({
super.key,
required this.child,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<Correspondent>( create: (context) => LabelCubit<Correspondent>(
context.read< context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
), ),
child: child, child: child,
); );

View File

@@ -13,8 +13,7 @@ class DocumentTypeBlocProvider extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<DocumentType>( create: (context) => LabelCubit<DocumentType>(
context context.read<LabelRepository<DocumentType>>(),
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
), ),
child: child, child: child,
); );

View File

@@ -18,25 +18,22 @@ class LabelsBlocProvider extends StatelessWidget {
providers: [ providers: [
BlocProvider<LabelCubit<StoragePath>>( BlocProvider<LabelCubit<StoragePath>>(
create: (context) => LabelCubit<StoragePath>( create: (context) => LabelCubit<StoragePath>(
context.read< context.read<LabelRepository<StoragePath>>(),
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
), ),
), ),
BlocProvider<LabelCubit<Correspondent>>( BlocProvider<LabelCubit<Correspondent>>(
create: (context) => LabelCubit<Correspondent>( create: (context) => LabelCubit<Correspondent>(
context.read< context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
), ),
), ),
BlocProvider<LabelCubit<DocumentType>>( BlocProvider<LabelCubit<DocumentType>>(
create: (context) => LabelCubit<DocumentType>( create: (context) => LabelCubit<DocumentType>(
context.read< context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
), ),
), ),
BlocProvider<LabelCubit<Tag>>( BlocProvider<LabelCubit<Tag>>(
create: (context) => LabelCubit<Tag>( create: (context) => LabelCubit<Tag>(
context.read<LabelRepository<Tag, TagRepositoryState>>(), context.read<LabelRepository<Tag>>(),
), ),
), ),
], ],

View File

@@ -13,8 +13,7 @@ class StoragePathBlocProvider extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<StoragePath>( create: (context) => LabelCubit<StoragePath>(
context context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
), ),
child: child, child: child,
); );

View File

@@ -13,7 +13,7 @@ class TagBlocProvider extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<Tag>( create: (context) => LabelCubit<Tag>(
context.read<LabelRepository<Tag, TagRepositoryState>>(), context.read<LabelRepository<Tag>>(),
), ),
child: child, child: child,
); );

View File

@@ -241,8 +241,7 @@ class _TagFormFieldState extends State<TagFormField> {
final Tag? tag = await Navigator.of(context).push<Tag>( final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => create: (context) => context.read<LabelRepository<Tag>>(),
context.read<LabelRepository<Tag, TagRepositoryState>>(),
child: AddTagPage(initialValue: _textEditingController.text), child: AddTagPage(initialValue: _textEditingController.text),
), ),
), ),

View File

@@ -82,7 +82,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
), ),
sliver: SearchAppBar( sliver: SearchAppBar(
hintText: "Search documents", //TODO: INTL hintText: S.of(context).documentSearchSearchDocuments,
onOpenSearch: showDocumentSearchPage, onOpenSearch: showDocumentSearchPage,
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
@@ -141,176 +141,138 @@ class _LabelsPageState extends State<LabelsPage>
} }
return true; return true;
}, },
child: MultiBlocProvider( child: RefreshIndicator(
providers: [ edgeOffset: kToolbarHeight + kTextTabBarHeight,
BlocProvider( notificationPredicate: (notification) =>
create: (context) => LabelCubit<Correspondent>( connectedState.isConnected,
context.read< onRefresh: () => [
LabelRepository<Correspondent, context.read<LabelCubit<Correspondent>>(),
CorrespondentRepositoryState>>(), context.read<LabelCubit<DocumentType>>(),
context.read<LabelCubit<Tag>>(),
context.read<LabelCubit<StoragePath>>(),
][_currentIndex]
.reload(),
child: TabBarView(
controller: _tabController,
children: [
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<Correspondent>(
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageCorrespondentEmptyStateDescriptionText,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
), ),
), Builder(
BlocProvider( builder: (context) {
create: (context) => LabelCubit<DocumentType>( return CustomScrollView(
context.read< slivers: [
LabelRepository<DocumentType, SliverOverlapInjector(
DocumentTypeRepositoryState>>(), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<DocumentType>(
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageDocumentTypeEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageDocumentTypeEmptyStateDescriptionText,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
), ),
), Builder(
BlocProvider( builder: (context) {
create: (context) => LabelCubit<Tag>( return CustomScrollView(
context slivers: [
.read<LabelRepository<Tag, TagRepositoryState>>(), SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<Tag>(
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel: S
.of(context)
.labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageTagsEmptyStateDescriptionText,
onAddNew: _openAddTagPage,
),
],
);
},
), ),
), Builder(
BlocProvider( builder: (context) {
create: (context) => LabelCubit<StoragePath>( return CustomScrollView(
context.read< slivers: [
LabelRepository<StoragePath, SliverOverlapInjector(
StoragePathRepositoryState>>(), handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<StoragePath>(
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
contentBuilder: (path) => Text(path.path ?? ""),
emptyStateActionButtonLabel: S
.of(context)
.labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
onAddNew: _openAddStoragePathPage,
),
],
);
},
), ),
), ],
],
child: RefreshIndicator(
edgeOffset: kToolbarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () => [
context.read<LabelCubit<Correspondent>>(),
context.read<LabelCubit<DocumentType>>(),
context.read<LabelCubit<Tag>>(),
context.read<LabelCubit<StoragePath>>(),
][_currentIndex]
.reload(),
child: TabBarView(
controller: _tabController,
children: [
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
LabelTabView<Correspondent>(
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageCorrespondentEmptyStateDescriptionText,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentTypeBlocProvider(
child: LabelTabView<DocumentType>(
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)
.labelsPageDocumentTypeEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageDocumentTypeEmptyStateDescriptionText,
onAddNew: _openAddDocumentTypePage,
),
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
TagBlocProvider(
child: LabelTabView<Tag>(
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel: S
.of(context)
.labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageTagsEmptyStateDescriptionText,
onAddNew: _openAddTagPage,
),
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
StoragePathBlocProvider(
child: LabelTabView<StoragePath>(
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
contentBuilder: (path) =>
Text(path.path ?? ""),
emptyStateActionButtonLabel: S
.of(context)
.labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
onAddNew: _openAddStoragePathPage,
),
),
],
);
},
),
],
),
), ),
), ),
), ),
@@ -326,8 +288,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: EditCorrespondentPage(correspondent: correspondent), child: EditCorrespondentPage(correspondent: correspondent),
), ),
), ),
@@ -339,8 +300,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: EditDocumentTypePage(documentType: docType), child: EditDocumentTypePage(documentType: docType),
), ),
), ),
@@ -352,8 +312,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => create: (context) => context.read<LabelRepository<Tag>>(),
context.read<LabelRepository<Tag, TagRepositoryState>>(),
child: EditTagPage(tag: tag), child: EditTagPage(tag: tag),
), ),
), ),
@@ -365,8 +324,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context create: (context) => context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: EditStoragePathPage( child: EditStoragePathPage(
storagePath: path, storagePath: path,
), ),
@@ -380,8 +338,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<Correspondent>>(),
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: const AddCorrespondentPage(), child: const AddCorrespondentPage(),
), ),
), ),
@@ -393,8 +350,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context.read< create: (context) => context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: const AddDocumentTypePage(), child: const AddDocumentTypePage(),
), ),
), ),
@@ -406,8 +362,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => create: (context) => context.read<LabelRepository<Tag>>(),
context.read<LabelRepository<Tag, TagRepositoryState>>(),
child: const AddTagPage(), child: const AddTagPage(),
), ),
), ),
@@ -419,8 +374,7 @@ class _LabelsPageState extends State<LabelsPage>
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => RepositoryProvider( builder: (_) => RepositoryProvider(
create: (context) => context create: (context) => context.read<LabelRepository<StoragePath>>(),
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: const AddStoragePathPage(), child: const AddStoragePathPage(),
), ),
), ),

View File

@@ -48,8 +48,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BlocProvider( builder: (context) => BlocProvider(
create: (context) => LinkedDocumentsCubit( create: (context) => LinkedDocumentsCubit(
context.read<PaperlessDocumentsApi>(),
filter, filter,
context.read(),
context.read(),
), ),
child: const LinkedDocumentsPage(), child: const LinkedDocumentsPage(),
), ),

View File

@@ -37,60 +37,65 @@ class LabelTabView<T extends Label> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocProvider(
builder: (context, connectivityState) { create: (context) => LabelCubit<T>(
return BlocBuilder<LabelCubit<T>, LabelState<T>>( context.read(),
builder: (context, state) { ),
if (!state.isLoaded && !connectivityState.isConnected) { child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
return const OfflineWidget(); builder: (context, connectivityState) {
} return BlocBuilder<LabelCubit<T>, LabelState<T>>(
final labels = state.labels.values.toList()..sort(); builder: (context, state) {
if (labels.isEmpty) { if (!state.isLoaded && !connectivityState.isConnected) {
return SliverFillRemaining( return const OfflineWidget();
child: Center( }
child: Column( final labels = state.labels.values.toList()..sort();
crossAxisAlignment: CrossAxisAlignment.center, if (labels.isEmpty) {
children: [ return SliverFillRemaining(
Text( child: Center(
emptyStateDescription, child: Column(
textAlign: TextAlign.center, crossAxisAlignment: CrossAxisAlignment.center,
), children: [
TextButton( Text(
onPressed: onAddNew, emptyStateDescription,
child: Text(emptyStateActionButtonLabel), textAlign: TextAlign.center,
), ),
].padded(), TextButton(
onPressed: onAddNew,
child: Text(emptyStateActionButtonLabel),
),
].padded(),
),
), ),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final l = labels.elementAt(index);
return LabelItem<T>(
name: l.name,
content: contentBuilder?.call(l) ??
Text(
translateMatchingAlgorithmName(
context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2,
),
onOpenEditPage: onEdit,
filterBuilder: filterBuilder,
leading: leadingBuilder?.call(l),
label: l,
);
},
childCount: labels.length,
), ),
); );
} },
return SliverList( );
delegate: SliverChildBuilderDelegate( },
(context, index) { ),
final l = labels.elementAt(index);
return LabelItem<T>(
name: l.name,
content: contentBuilder?.call(l) ??
Text(
translateMatchingAlgorithmName(
context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2,
),
onOpenEditPage: onEdit,
filterBuilder: filterBuilder,
leading: leadingBuilder?.call(l),
label: l,
);
},
childCount: labels.length,
),
);
},
);
},
); );
} }
} }

View File

@@ -2,13 +2,10 @@ import 'package:flutter/material.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/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.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/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
class LabelText<T extends Label, State extends RepositoryState> class LabelText<T extends Label> extends StatelessWidget {
extends StatelessWidget {
final int? id; final int? id;
final String placeholder; final String placeholder;
final TextStyle? style; final TextStyle? style;
@@ -24,7 +21,7 @@ class LabelText<T extends Label, State extends RepositoryState>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<T>( create: (context) => LabelCubit<T>(
context.read<LabelRepository<T, State>>(), context.read<LabelRepository<T>>(),
), ),
child: BlocBuilder<LabelCubit<T>, LabelState<T>>( child: BlocBuilder<LabelCubit<T>, LabelState<T>>(
builder: (context, state) { builder: (context, state) {

View File

@@ -13,10 +13,25 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
final DocumentChangedNotifier notifier; final DocumentChangedNotifier notifier;
LinkedDocumentsCubit( LinkedDocumentsCubit(
this.api,
DocumentFilter filter, DocumentFilter filter,
this.api,
this.notifier, this.notifier,
) : super(const LinkedDocumentsState()) { ) : super(const LinkedDocumentsState()) {
updateFilter(filter: filter); updateFilter(filter: filter);
notifier.subscribe(
this,
onUpdated: replace,
onDeleted: remove,
);
}
@override
Future<void> update(DocumentModel document) async {
final updated = await api.update(document);
if (!state.filter.matches(updated)) {
remove(document);
} else {
replace(document);
}
} }
} }

View File

@@ -2,11 +2,7 @@ import 'package:flutter/material.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/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart'; import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.dart'; import 'package:paperless_mobile/features/linked_documents/bloc/linked_documents_cubit.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/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -60,18 +56,15 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
isLabelClickable: false, isLabelClickable: false,
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
onTap: (document) async { onTap: (document) {
final updatedDocument = await Navigator.pushNamed( Navigator.pushNamed(
context, context,
DocumentDetailsRoute.routeName, DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments( arguments: DocumentDetailsRouteArguments(
document: document, document: document,
isLabelClickable: false, isLabelClickable: false,
), ),
) as DocumentModel?; );
if (updatedDocument != document) {
context.read<LinkedDocumentsCubit>().reload();
}
}, },
); );
}, },

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/notifications/services/models/notification_payloads/open_created_document_notification_payload.dart'; import 'package:paperless_mobile/features/notifications/services/models/notification_payloads/open_created_document_notification_payload.dart';
@@ -121,7 +122,7 @@ class LocalNotificationService {
) {} ) {}
void onDidReceiveNotificationResponse(NotificationResponse response) { void onDidReceiveNotificationResponse(NotificationResponse response) {
log("Received Notification: ${response.payload}"); debugPrint("Received Notification: ${response.payload}");
if (response.notificationResponseType == if (response.notificationResponseType ==
NotificationResponseType.selectedNotificationAction) { NotificationResponseType.selectedNotificationAction) {
final action = final action =

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
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';
@@ -73,14 +75,18 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
try { try {
final filter = state.filter.copyWith(page: 1); final filter = state.filter.copyWith(page: 1);
final result = await api.findAll(filter); final result = await api.findAll(filter);
emit(state.copyWithPaged( if (!isClosed) {
hasLoaded: true, emit(state.copyWithPaged(
value: [result], hasLoaded: true,
isLoading: false, value: [result],
filter: filter, isLoading: false,
)); filter: filter,
));
}
} finally { } finally {
emit(state.copyWithPaged(isLoading: false)); if (!isClosed) {
emit(state.copyWithPaged(isLoading: false));
}
} }
} }
@@ -88,16 +94,10 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
/// Updates a document. If [shouldReload] is false, the updated document will /// Updates a document. If [shouldReload] is false, the updated document will
/// replace the currently loaded one, otherwise all documents will be reloaded. /// replace the currently loaded one, otherwise all documents will be reloaded.
/// ///
Future<void> update( Future<void> update(DocumentModel document) async {
DocumentModel document, {
bool shouldReload = true,
}) async {
final updatedDocument = await api.update(document); final updatedDocument = await api.update(document);
if (shouldReload) { notifier.notifyUpdated(updatedDocument);
await reload(); // replace(updatedDocument);
} else {
replace(updatedDocument);
}
} }
/// ///
@@ -107,7 +107,8 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
emit(state.copyWithPaged(isLoading: true)); emit(state.copyWithPaged(isLoading: true));
try { try {
await api.delete(document); await api.delete(document);
await remove(document); notifier.notifyDeleted(document);
// remove(document); // Removing deleted now works with the change notifier.
} finally { } finally {
emit(state.copyWithPaged(isLoading: false)); emit(state.copyWithPaged(isLoading: false));
} }
@@ -117,7 +118,7 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
/// Removes [document] from the currently loaded state. /// Removes [document] from the currently loaded state.
/// Does not delete it from the server! /// Does not delete it from the server!
/// ///
Future<void> remove(DocumentModel document) async { void remove(DocumentModel document) {
final index = state.value.indexWhere( final index = state.value.indexWhere(
(page) => page.results.any((element) => element.id == document.id), (page) => page.results.any((element) => element.id == document.id),
); );
@@ -144,23 +145,36 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
/// ///
/// Replaces the document with the same id as [document] from the currently /// Replaces the document with the same id as [document] from the currently
/// loaded state. /// loaded state if the document's properties still match the given filter criteria, otherwise removes it.
/// ///
Future<void> replace(DocumentModel document) async { Future<void> replace(DocumentModel document) async {
final index = state.value.indexWhere( final matchesFilterCriteria = state.filter.matches(document);
if (!matchesFilterCriteria) {
return remove(document);
}
final pageIndex = state.value.indexWhere(
(page) => page.results.any((element) => element.id == document.id), (page) => page.results.any((element) => element.id == document.id),
); );
if (index != -1) { if (pageIndex != -1) {
final foundPage = state.value[index]; final foundPage = state.value[pageIndex];
final replacementPage = foundPage.copyWith( final replacementPage = foundPage.copyWith(
results: foundPage.results..replaceRange(index, index + 1, [document]), results: foundPage.results
.map((doc) => doc.id == document.id ? document : doc)
.toList(),
); );
emit(state.copyWithPaged( final newState = state.copyWithPaged(
value: state.value value: state.value
.mapIndexed((currIndex, element) => .mapIndexed((currIndex, element) =>
currIndex == index ? replacementPage : element) currIndex == pageIndex ? replacementPage : element)
.toList(), .toList(),
)); );
emit(newState);
} }
} }
@override
Future<void> close() {
notifier.unsubscribe(this);
return super.close();
}
} }

View File

@@ -34,7 +34,9 @@ class SavedViewCubit extends Cubit<SavedViewState> {
Future<void> initialize() async { Future<void> initialize() async {
final views = await _repository.findAll(); final views = await _repository.findAll();
final values = {for (var element in views) element.id!: element}; final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values, hasLoaded: true)); if (!isClosed) {
emit(SavedViewState(value: values, hasLoaded: true));
}
} }
Future<void> reload() => initialize(); Future<void> reload() => initialize();

View File

@@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/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/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_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';
@@ -10,11 +11,20 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
final SavedView savedView; final SavedView savedView;
SavedViewDetailsCubit( SavedViewDetailsCubit(
this.api, { this.api,
this.notifier, {
required this.savedView, required this.savedView,
}) : super(const SavedViewDetailsState()) { }) : super(const SavedViewDetailsState()) {
notifier.subscribe(
this,
onDeleted: remove,
onUpdated: replace,
);
updateFilter(filter: savedView.toDocumentFilter()); updateFilter(filter: savedView.toDocumentFilter());
} }
} }

View File

@@ -42,6 +42,7 @@ class SavedViewList extends StatelessWidget {
providers: [ providers: [
BlocProvider( BlocProvider(
create: (context) => SavedViewDetailsCubit( create: (context) => SavedViewDetailsCubit(
context.read(),
context.read(), context.read(),
savedView: view, savedView: view,
), ),

View File

@@ -117,18 +117,14 @@ class _SavedViewPageState extends State<SavedViewPage> {
); );
} }
void _onOpenDocumentDetails(DocumentModel document) async { void _onOpenDocumentDetails(DocumentModel document) {
final updatedDocument = await Navigator.pushNamed( Navigator.pushNamed(
context, context,
DocumentDetailsRoute.routeName, DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments( arguments: DocumentDetailsRouteArguments(
document: document, document: document,
isLabelClickable: false, isLabelClickable: false,
), ),
) as DocumentModel?; );
if (updatedDocument != document) {
// Reload in case document was edited and might not fulfill filter criteria of saved view anymore
context.read<SavedViewDetailsCubit>().reload();
}
} }
} }

View File

@@ -12,9 +12,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/global/constants.dart'; import 'package:paperless_mobile/core/global/constants.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/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.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/tag_repository_state.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart'; import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
@@ -46,6 +43,15 @@ class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final safeAreaPadding = MediaQuery.of(context).padding;
final availableHeight = MediaQuery.of(context).size.height -
2 * kToolbarHeight -
kTextTabBarHeight -
kBottomNavigationBarHeight -
safeAreaPadding.top -
safeAreaPadding.bottom;
print(availableHeight);
return BlocBuilder<ConnectivityCubit, ConnectivityState>( return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) { builder: (context, connectedState) {
return Scaffold( return Scaffold(
@@ -61,7 +67,33 @@ class _ScannerPageState extends State<ScannerPage>
// ), // ),
body: BlocBuilder<DocumentScannerCubit, List<File>>( body: BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
return NestedScrollView( return CustomScrollView(
physics:
state.isEmpty ? const NeverScrollableScrollPhysics() : null,
slivers: [
SearchAppBar(
hintText: S.of(context).documentSearchSearchDocuments,
onOpenSearch: showDocumentSearchPage,
bottom: PreferredSize(
child: _buildActions(connectedState.isConnected),
preferredSize: const Size.fromHeight(kTextTabBarHeight),
),
),
if (state.isEmpty)
SliverToBoxAdapter(
child: SizedBox(
height: availableHeight,
child: Center(
child: _buildEmptyState(connectedState.isConnected),
),
),
)
else
_buildImageGrid(state)
],
);
NestedScrollView(
floatHeaderSlivers: false, floatHeaderSlivers: false,
headerSliverBuilder: (context, innerBoxIsScrolled) => [ headerSliverBuilder: (context, innerBoxIsScrolled) => [
SearchAppBar( SearchAppBar(
@@ -76,8 +108,9 @@ class _ScannerPageState extends State<ScannerPage>
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
if (state.isEmpty) if (state.isEmpty)
SliverFillRemaining( SliverFillViewport(
child: _buildEmptyState(connectedState.isConnected), delegate: SliverChildListDelegate.fixed(
[_buildEmptyState(connectedState.isConnected)]),
) )
else else
_buildImageGrid(state) _buildImageGrid(state)
@@ -229,13 +262,11 @@ class _ScannerPageState extends State<ScannerPage>
child: BlocProvider( child: BlocProvider(
create: (context) => DocumentUploadCubit( create: (context) => DocumentUploadCubit(
documentApi: context.read<PaperlessDocumentsApi>(), documentApi: context.read<PaperlessDocumentsApi>(),
correspondentRepository: context.read< correspondentRepository:
LabelRepository<Correspondent, context.read<LabelRepository<Correspondent>>(),
CorrespondentRepositoryState>>(), documentTypeRepository:
documentTypeRepository: context.read< context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(), tagRepository: context.read<LabelRepository<Tag>>(),
tagRepository:
context.read<LabelRepository<Tag, TagRepositoryState>>(),
), ),
child: DocumentUploadPreparationPage( child: DocumentUploadPreparationPage(
fileBytes: file.bytes, fileBytes: file.bytes,
@@ -346,14 +377,11 @@ class _ScannerPageState extends State<ScannerPage>
child: BlocProvider( child: BlocProvider(
create: (context) => DocumentUploadCubit( create: (context) => DocumentUploadCubit(
documentApi: context.read<PaperlessDocumentsApi>(), documentApi: context.read<PaperlessDocumentsApi>(),
correspondentRepository: context.read< correspondentRepository:
LabelRepository<Correspondent, context.read<LabelRepository<Correspondent>>(),
CorrespondentRepositoryState>>(), documentTypeRepository:
documentTypeRepository: context.read< context.read<LabelRepository<DocumentType>>(),
LabelRepository<DocumentType, tagRepository: context.read<LabelRepository<Tag>>(),
DocumentTypeRepositoryState>>(),
tagRepository:
context.read<LabelRepository<Tag, TagRepositoryState>>(),
), ),
child: DocumentUploadPreparationPage( child: DocumentUploadPreparationPage(
fileBytes: file.readAsBytesSync(), fileBytes: file.readAsBytesSync(),

View File

@@ -6,10 +6,6 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.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/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_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';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.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/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
@@ -26,9 +22,10 @@ class AccountSettingsDialog extends StatelessWidget {
scrollable: true, scrollable: true,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const CloseButton(),
Text(S.of(context).accountSettingsTitle), Text(S.of(context).accountSettingsTitle),
const CloseButton(),
], ],
), ),
content: BlocBuilder<PaperlessServerInformationCubit, content: BlocBuilder<PaperlessServerInformationCubit,
@@ -55,7 +52,7 @@ class AccountSettingsDialog extends StatelessWidget {
ListTile( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.person_add_rounded), leading: const Icon(Icons.person_add_rounded),
title: const Text("Add another account"), //TODO: INTL title: Text(S.of(context).accountSettingsAddAnotherAccount),
onTap: () {}, onTap: () {},
), ),
Divider(), Divider(),
@@ -87,16 +84,10 @@ class AccountSettingsDialog extends StatelessWidget {
try { try {
await context.read<AuthenticationCubit>().logout(); await context.read<AuthenticationCubit>().logout();
await context.read<ApplicationSettingsCubit>().clear(); await context.read<ApplicationSettingsCubit>().clear();
await context.read<LabelRepository<Tag, TagRepositoryState>>().clear(); await context.read<LabelRepository<Tag>>().clear();
await context await context.read<LabelRepository<Correspondent>>().clear();
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>() await context.read<LabelRepository<DocumentType>>().clear();
.clear(); await context.read<LabelRepository<StoragePath>>().clear();
await context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
.clear();
await context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
.clear();
await context.read<SavedViewRepository>().clear(); await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear(); await HydratedBloc.storage.clear();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {

View File

@@ -1,21 +1,32 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/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/paged_document_view/paged_documents_mixin.dart'; import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart'; import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
part 'similar_documents_state.dart'; part 'similar_documents_state.dart';
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState> class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
with PagedDocumentsMixin<SimilarDocumentsState> { with PagedDocumentsMixin {
final int documentId; final int documentId;
@override @override
final PaperlessDocumentsApi api; final PaperlessDocumentsApi api;
@override
final DocumentChangedNotifier notifier;
SimilarDocumentsCubit( SimilarDocumentsCubit(
this.api, { this.api,
this.notifier, {
required this.documentId, required this.documentId,
}) : super(const SimilarDocumentsState()); }) : super(const SimilarDocumentsState()) {
notifier.subscribe(
this,
onDeleted: remove,
onUpdated: replace,
);
}
Future<void> initialize() async { Future<void> initialize() async {
if (!state.hasLoaded) { if (!state.hasLoaded) {

View File

@@ -2,14 +2,13 @@ import 'package:flutter/material.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/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart'; import 'package:paperless_mobile/features/documents/view/widgets/items/document_list_item.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart'; import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/routes/document_details_route.dart';
class SimilarDocumentsView extends StatefulWidget { class SimilarDocumentsView extends StatefulWidget {
const SimilarDocumentsView({super.key}); const SimilarDocumentsView({super.key});
@@ -54,13 +53,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const earlyPreviewHintCard = HintCard(
hintIcon: Icons.construction,
hintText: "This view is still work in progress.",
);
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>( return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
builder: (context, state) { builder: (context, state) {
if (state.documents.isEmpty) { if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
return DocumentsEmptyState( return DocumentsEmptyState(
state: state, state: state,
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter( onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
@@ -77,26 +72,23 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
return CustomScrollView( return CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
const SliverToBoxAdapter(child: earlyPreviewHintCard),
SliverAdaptiveDocumentsView( SliverAdaptiveDocumentsView(
documents: state.documents, documents: state.documents,
hasInternetConnection: connectivity.isConnected, hasInternetConnection: connectivity.isConnected,
isLabelClickable: false, isLabelClickable: false,
isLoading: state.isLoading, isLoading: state.isLoading,
hasLoaded: state.hasLoaded, hasLoaded: state.hasLoaded,
enableHeroAnimation: false,
), onTap: (document) {
SliverList( Navigator.pushNamed(
delegate: SliverChildBuilderDelegate( context,
childCount: state.documents.length, DocumentDetailsRoute.routeName,
(context, index) => DocumentListItem( arguments: DocumentDetailsRouteArguments(
document: state.documents[index], document: document,
enableHeroAnimation: false, isLabelClickable: false,
isLabelClickable: false, ),
isSelected: false, );
isSelectionActive: false, },
),
),
), ),
], ],
); );

View File

@@ -6,6 +6,8 @@
"name": {} "name": {}
} }
}, },
"accountSettingsAddAnotherAccount": "Add another account",
"@accountSettingsAddAnotherAccount": {},
"accountSettingsTitle": "Account", "accountSettingsTitle": "Account",
"@accountSettingsTitle": {}, "@accountSettingsTitle": {},
"addCorrespondentPageTitle": "Nový korespondent", "addCorrespondentPageTitle": "Nový korespondent",
@@ -396,6 +398,10 @@
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Jste offline.", "genericMessageOfflineText": "Jste offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxActionAssignAsn": "Assign ASN",
"@inboxActionAssignAsn": {},
"inboxActionDeleteDocument": "Delete document",
"@inboxActionDeleteDocument": {},
"inboxPageAssignAsnLabel": "Assign ASN", "inboxPageAssignAsnLabel": "Assign ASN",
"@inboxPageAssignAsnLabel": {}, "@inboxPageAssignAsnLabel": {},
"inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.", "inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.",

View File

@@ -6,6 +6,8 @@
"name": {} "name": {}
} }
}, },
"accountSettingsAddAnotherAccount": "Einen Account hinzufügen",
"@accountSettingsAddAnotherAccount": {},
"accountSettingsTitle": "Account", "accountSettingsTitle": "Account",
"@accountSettingsTitle": {}, "@accountSettingsTitle": {},
"addCorrespondentPageTitle": "Neuer Korrespondent", "addCorrespondentPageTitle": "Neuer Korrespondent",
@@ -396,6 +398,10 @@
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Du bist offline.", "genericMessageOfflineText": "Du bist offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxActionAssignAsn": "ASN zuweisen",
"@inboxActionAssignAsn": {},
"inboxActionDeleteDocument": "Dokument löschen",
"@inboxActionDeleteDocument": {},
"inboxPageAssignAsnLabel": "ASN zuweisen", "inboxPageAssignAsnLabel": "ASN zuweisen",
"@inboxPageAssignAsnLabel": {}, "@inboxPageAssignAsnLabel": {},
"inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.",

View File

@@ -6,6 +6,8 @@
"name": {} "name": {}
} }
}, },
"accountSettingsAddAnotherAccount": "Add another account",
"@accountSettingsAddAnotherAccount": {},
"accountSettingsTitle": "Account", "accountSettingsTitle": "Account",
"@accountSettingsTitle": {}, "@accountSettingsTitle": {},
"addCorrespondentPageTitle": "New Correspondent", "addCorrespondentPageTitle": "New Correspondent",
@@ -396,6 +398,10 @@
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "You're offline.", "genericMessageOfflineText": "You're offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxActionAssignAsn": "Assign ASN",
"@inboxActionAssignAsn": {},
"inboxActionDeleteDocument": "Delete document",
"@inboxActionDeleteDocument": {},
"inboxPageAssignAsnLabel": "Assign ASN", "inboxPageAssignAsnLabel": "Assign ASN",
"@inboxPageAssignAsnLabel": {}, "@inboxPageAssignAsnLabel": {},
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.", "inboxPageDocumentRemovedMessageText": "Document removed from inbox.",

View File

@@ -6,6 +6,8 @@
"name": {} "name": {}
} }
}, },
"accountSettingsAddAnotherAccount": "Add another account",
"@accountSettingsAddAnotherAccount": {},
"accountSettingsTitle": "Account", "accountSettingsTitle": "Account",
"@accountSettingsTitle": {}, "@accountSettingsTitle": {},
"addCorrespondentPageTitle": "New Correspondent", "addCorrespondentPageTitle": "New Correspondent",
@@ -396,6 +398,10 @@
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Jesteście w trybie offline.", "genericMessageOfflineText": "Jesteście w trybie offline.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxActionAssignAsn": "Assign ASN",
"@inboxActionAssignAsn": {},
"inboxActionDeleteDocument": "Delete document",
"@inboxActionDeleteDocument": {},
"inboxPageAssignAsnLabel": "Assign ASN", "inboxPageAssignAsnLabel": "Assign ASN",
"@inboxPageAssignAsnLabel": {}, "@inboxPageAssignAsnLabel": {},
"inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej", "inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej",

View File

@@ -6,6 +6,8 @@
"name": {} "name": {}
} }
}, },
"accountSettingsAddAnotherAccount": "Add another account",
"@accountSettingsAddAnotherAccount": {},
"accountSettingsTitle": "Account", "accountSettingsTitle": "Account",
"@accountSettingsTitle": {}, "@accountSettingsTitle": {},
"addCorrespondentPageTitle": "Yeni ek yazar", "addCorrespondentPageTitle": "Yeni ek yazar",
@@ -396,6 +398,10 @@
"@genericActionUploadLabel": {}, "@genericActionUploadLabel": {},
"genericMessageOfflineText": "Çevrimdışısınız.", "genericMessageOfflineText": "Çevrimdışısınız.",
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxActionAssignAsn": "Assign ASN",
"@inboxActionAssignAsn": {},
"inboxActionDeleteDocument": "Delete document",
"@inboxActionDeleteDocument": {},
"inboxPageAssignAsnLabel": "ASN ata", "inboxPageAssignAsnLabel": "ASN ata",
"@inboxPageAssignAsnLabel": {}, "@inboxPageAssignAsnLabel": {},
"inboxPageDocumentRemovedMessageText": "Döküman gelen kutusundan kaldırıldı.", "inboxPageDocumentRemovedMessageText": "Döküman gelen kutusundan kaldırıldı.",

View File

@@ -109,7 +109,7 @@ void main() async {
final connectivityCubit = ConnectivityCubit(connectivityStatusService); final connectivityCubit = ConnectivityCubit(connectivityStatusService);
// Remove temporarily downloaded files. // Remove temporarily downloaded files.
(await FileService.temporaryDirectory).deleteSync(recursive: true); // (await FileService.temporaryDirectory).deleteSync(recursive: true);
// Load application settings and stored authentication data // Load application settings and stored authentication data
await connectivityCubit.initialize(); await connectivityCubit.initialize();
@@ -173,20 +173,16 @@ void main() async {
], ],
child: MultiRepositoryProvider( child: MultiRepositoryProvider(
providers: [ providers: [
RepositoryProvider<LabelRepository<Tag, TagRepositoryState>>.value( RepositoryProvider<LabelRepository<Tag>>.value(
value: tagRepository, value: tagRepository,
), ),
RepositoryProvider< RepositoryProvider<LabelRepository<Correspondent>>.value(
LabelRepository<Correspondent,
CorrespondentRepositoryState>>.value(
value: correspondentRepository, value: correspondentRepository,
), ),
RepositoryProvider< RepositoryProvider<LabelRepository<DocumentType>>.value(
LabelRepository<DocumentType, DocumentTypeRepositoryState>>.value(
value: documentTypeRepository, value: documentTypeRepository,
), ),
RepositoryProvider< RepositoryProvider<LabelRepository<StoragePath>>.value(
LabelRepository<StoragePath, StoragePathRepositoryState>>.value(
value: storagePathRepository, value: storagePathRepository,
), ),
RepositoryProvider<SavedViewRepository>.value( RepositoryProvider<SavedViewRepository>.value(

View File

@@ -17,8 +17,9 @@ class DocumentDetailsRoute extends StatelessWidget {
return BlocProvider( return BlocProvider(
create: (context) => DocumentDetailsCubit( create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(), context.read(),
args.document, context.read(),
initialDocument: args.document,
), ),
child: LabelRepositoriesProvider( child: LabelRepositoriesProvider(
child: DocumentDetailsPage( child: DocumentDetailsPage(

View File

@@ -146,7 +146,20 @@ class DocumentFilter extends Equatable {
/// ///
/// Checks whether the properties of [document] match the current filter criteria. /// Checks whether the properties of [document] match the current filter criteria.
/// ///
bool includes(DocumentModel document) {} bool matches(DocumentModel document) {
return correspondent.matches(document.correspondent) &&
documentType.matches(document.documentType) &&
storagePath.matches(document.storagePath) &&
tags.matches(document.tags) &&
created.matches(document.created) &&
added.matches(document.added) &&
modified.matches(document.modified) &&
query.matches(
title: document.title,
content: document.content,
asn: document.archiveSerialNumber,
);
}
int get appliedFiltersCount => [ int get appliedFiltersCount => [
documentType != initial.documentType, documentType != initial.documentType,

View File

@@ -1,6 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/document_model.dart';
const pageRegex = r".*page=(\d+).*"; const pageRegex = r".*page=(\d+).*";
@@ -108,5 +107,10 @@ class PagedSearchResult<T> extends Equatable {
} }
@override @override
List<Object?> get props => [count, next, previous, results]; List<Object?> get props => [
count,
next,
previous,
results,
];
} }

View File

@@ -52,4 +52,17 @@ class AbsoluteDateRangeQuery extends DateRangeQuery {
@override @override
Map<String, dynamic> toJson() => _$AbsoluteDateRangeQueryToJson(this); Map<String, dynamic> toJson() => _$AbsoluteDateRangeQueryToJson(this);
@override
bool matches(DateTime dt) {
//TODO: Check if after and before are inclusive or exclusive definitions.
bool matches = true;
if (after != null) {
matches &= dt.isAfter(after!) || dt == after;
}
if (before != null) {
matches &= dt.isBefore(before!) || dt == before;
}
return matches;
}
} }

View File

@@ -7,4 +7,6 @@ abstract class DateRangeQuery extends Equatable {
Map<String, String> toQueryParameter(DateRangeQueryField field); Map<String, String> toQueryParameter(DateRangeQueryField field);
Map<String, dynamic> toJson(); Map<String, dynamic> toJson();
bool matches(DateTime dt);
} }

Some files were not shown because too many files have changed in this diff Show More