mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 01:15:44 -06:00
Cleaned up code, implemented message queue to notify subscribers of document updates.
This commit is contained in:
@@ -68,11 +68,11 @@ android {
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ PODS:
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- edge_detection (1.0.9):
|
||||
- edge_detection (1.1.1):
|
||||
- Flutter
|
||||
- WeScan
|
||||
- file_picker (0.0.1):
|
||||
@@ -44,6 +44,8 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
@@ -56,10 +58,13 @@ PODS:
|
||||
- Flutter
|
||||
- local_auth_ios (0.0.1):
|
||||
- Flutter
|
||||
- open_filex (0.0.2):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- pdfx (1.0.0):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.0.4):
|
||||
@@ -72,8 +77,9 @@ PODS:
|
||||
- SDWebImage/Core (5.13.5)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
@@ -90,17 +96,19 @@ DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- 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`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/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`)
|
||||
- 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`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/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`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
@@ -128,6 +136,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
fluttertoast:
|
||||
@@ -136,10 +146,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
local_auth_ios:
|
||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||
open_filex:
|
||||
:path: ".symlinks/plugins/open_filex/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
pdfx:
|
||||
:path: ".symlinks/plugins/pdfx/ios"
|
||||
permission_handler_apple:
|
||||
@@ -148,8 +160,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
url_launcher_ios:
|
||||
@@ -160,28 +172,30 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
edge_detection: 9bc5ee35073b5a17c0b3b679908f01017ce3062a
|
||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
||||
edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
|
||||
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||
pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec
|
||||
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 51;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -321,10 +321,12 @@
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
@@ -335,6 +337,7 @@
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
||||
@@ -65,5 +65,7 @@
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:rxdart/subjects.dart';
|
||||
|
||||
@@ -9,26 +11,40 @@ class DocumentChangedNotifier {
|
||||
final Subject<DocumentModel> _updated = PublishSubject();
|
||||
final Subject<DocumentModel> _deleted = PublishSubject();
|
||||
|
||||
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
||||
|
||||
void notifyUpdated(DocumentModel updated) {
|
||||
debugPrint("Notifying updated document ${updated.id}");
|
||||
_updated.add(updated);
|
||||
}
|
||||
|
||||
void notifyDeleted(DocumentModel deleted) {
|
||||
debugPrint("Notifying deleted document ${deleted.id}");
|
||||
_deleted.add(deleted);
|
||||
}
|
||||
|
||||
List<StreamSubscription> listen({
|
||||
void subscribe(
|
||||
dynamic subscriber, {
|
||||
DocumentChangedCallback? onUpdated,
|
||||
DocumentChangedCallback? onDeleted,
|
||||
}) {
|
||||
return [
|
||||
_updated.listen((value) {
|
||||
onUpdated?.call(value);
|
||||
}),
|
||||
_updated.listen((value) {
|
||||
onDeleted?.call(value);
|
||||
}),
|
||||
];
|
||||
_subscribers.putIfAbsent(
|
||||
subscriber,
|
||||
() => [
|
||||
_updated.listen((value) {
|
||||
onUpdated?.call(value);
|
||||
}),
|
||||
_deleted.listen((value) {
|
||||
onDeleted?.call(value);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void unsubscribe(dynamic subscriber) {
|
||||
_subscribers[subscriber]?.forEach((element) {
|
||||
element.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
void close() {
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
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';
|
||||
|
||||
///
|
||||
/// Base repository class which all repositories should implement
|
||||
///
|
||||
abstract class BaseRepository<State extends RepositoryState, Type>
|
||||
extends Cubit<State> with HydratedMixin {
|
||||
final State _initialState;
|
||||
abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
|
||||
with HydratedMixin {
|
||||
final IndexedRepositoryState<T> _initialState;
|
||||
|
||||
BaseRepository(this._initialState) : super(_initialState) {
|
||||
hydrate();
|
||||
}
|
||||
|
||||
Stream<State?> get values =>
|
||||
Stream<IndexedRepositoryState<T>?> get values =>
|
||||
BehaviorSubject.seeded(state)..addStream(super.stream);
|
||||
|
||||
State? get current => state;
|
||||
IndexedRepositoryState<T>? get current => state;
|
||||
|
||||
bool get isInitialized => state.hasLoaded;
|
||||
|
||||
Future<Type> create(Type object);
|
||||
Future<Type?> find(int id);
|
||||
Future<Iterable<Type>> findAll([Iterable<int>? ids]);
|
||||
Future<Type> update(Type object);
|
||||
Future<int> delete(Type object);
|
||||
Future<T> create(T object);
|
||||
Future<T?> find(int id);
|
||||
Future<Iterable<T>> findAll([Iterable<int>? ids]);
|
||||
Future<T> update(T object);
|
||||
Future<int> delete(T object);
|
||||
|
||||
@override
|
||||
Future<void> clear() async {
|
||||
|
||||
@@ -3,10 +3,8 @@ import 'dart:async';
|
||||
import 'package:paperless_api/paperless_api.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/repository_state.dart';
|
||||
|
||||
class CorrespondentRepositoryImpl
|
||||
extends LabelRepository<Correspondent, CorrespondentRepositoryState> {
|
||||
class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
|
||||
final PaperlessLabelsApi _api;
|
||||
|
||||
CorrespondentRepositoryImpl(this._api)
|
||||
@@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl
|
||||
@override
|
||||
Future<Correspondent> create(Correspondent correspondent) async {
|
||||
final created = await _api.saveCorrespondent(correspondent);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return created;
|
||||
@@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl
|
||||
@override
|
||||
Future<int> delete(Correspondent correspondent) async {
|
||||
await _api.deleteCorrespondent(correspondent);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..removeWhere((k, v) => k == correspondent.id);
|
||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return correspondent.id!;
|
||||
@@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl
|
||||
Future<Correspondent?> find(int id) async {
|
||||
final correspondent = await _api.getCorrespondent(id);
|
||||
if (correspondent != null) {
|
||||
final updatedState = {...state.values}..[id] = correspondent;
|
||||
final updatedState = {...state.values ?? {}}..[id] = correspondent;
|
||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return correspondent;
|
||||
}
|
||||
@@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl
|
||||
@override
|
||||
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
|
||||
final correspondents = await _api.getCorrespondents(ids);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return correspondents;
|
||||
@@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl
|
||||
@override
|
||||
Future<Correspondent> update(Correspondent correspondent) async {
|
||||
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));
|
||||
return updated;
|
||||
}
|
||||
@@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(CorrespondentRepositoryState state) {
|
||||
Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:paperless_api/paperless_api.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:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
|
||||
class DocumentTypeRepositoryImpl
|
||||
extends LabelRepository<DocumentType, DocumentTypeRepositoryState> {
|
||||
class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
|
||||
final PaperlessLabelsApi _api;
|
||||
|
||||
DocumentTypeRepositoryImpl(this._api)
|
||||
@@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl
|
||||
@override
|
||||
Future<DocumentType> create(DocumentType documentType) async {
|
||||
final created = await _api.saveDocumentType(documentType);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return created;
|
||||
@@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl
|
||||
@override
|
||||
Future<int> delete(DocumentType documentType) async {
|
||||
await _api.deleteDocumentType(documentType);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..removeWhere((k, v) => k == documentType.id);
|
||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return documentType.id!;
|
||||
@@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl
|
||||
Future<DocumentType?> find(int id) async {
|
||||
final documentType = await _api.getDocumentType(id);
|
||||
if (documentType != null) {
|
||||
final updatedState = {...state.values}..[id] = documentType;
|
||||
final updatedState = {...state.values ?? {}}..[id] = documentType;
|
||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return documentType;
|
||||
}
|
||||
@@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl
|
||||
@override
|
||||
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
|
||||
final documentTypes = await _api.getDocumentTypes(ids);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return documentTypes;
|
||||
@@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl
|
||||
@override
|
||||
Future<DocumentType> update(DocumentType documentType) async {
|
||||
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));
|
||||
return updated;
|
||||
}
|
||||
@@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(DocumentTypeRepositoryState state) {
|
||||
Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
||||
@override
|
||||
Future<SavedView> create(SavedView object) async {
|
||||
final created = await _api.save(object);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return created;
|
||||
@@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
||||
@override
|
||||
Future<int> delete(SavedView view) async {
|
||||
await _api.delete(view);
|
||||
final updatedState = {...state.values}..remove(view.id);
|
||||
final updatedState = {...state.values ?? {}}..remove(view.id);
|
||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return view.id!;
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
||||
@override
|
||||
Future<SavedView?> find(int id) async {
|
||||
final found = await _api.find(id);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..update(id, (_) => found, ifAbsent: () => found);
|
||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return found;
|
||||
@@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
||||
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
|
||||
final found = await _api.findAll(ids);
|
||||
final updatedState = {
|
||||
...state.values,
|
||||
...state.values ?? {},
|
||||
...{for (final view in found) view.id!: view},
|
||||
};
|
||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||
@@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(SavedViewRepositoryState state) {
|
||||
Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
|
||||
class StoragePathRepositoryImpl
|
||||
extends LabelRepository<StoragePath, StoragePathRepositoryState> {
|
||||
class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
|
||||
final PaperlessLabelsApi _api;
|
||||
|
||||
StoragePathRepositoryImpl(this._api)
|
||||
@@ -13,7 +12,7 @@ class StoragePathRepositoryImpl
|
||||
@override
|
||||
Future<StoragePath> create(StoragePath storagePath) async {
|
||||
final created = await _api.saveStoragePath(storagePath);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return created;
|
||||
@@ -22,7 +21,7 @@ class StoragePathRepositoryImpl
|
||||
@override
|
||||
Future<int> delete(StoragePath storagePath) async {
|
||||
await _api.deleteStoragePath(storagePath);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..removeWhere((k, v) => k == storagePath.id);
|
||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return storagePath.id!;
|
||||
@@ -32,7 +31,7 @@ class StoragePathRepositoryImpl
|
||||
Future<StoragePath?> find(int id) async {
|
||||
final storagePath = await _api.getStoragePath(id);
|
||||
if (storagePath != null) {
|
||||
final updatedState = {...state.values}..[id] = storagePath;
|
||||
final updatedState = {...state.values ?? {}}..[id] = storagePath;
|
||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return storagePath;
|
||||
}
|
||||
@@ -42,7 +41,7 @@ class StoragePathRepositoryImpl
|
||||
@override
|
||||
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
|
||||
final storagePaths = await _api.getStoragePaths(ids);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return storagePaths;
|
||||
@@ -51,7 +50,8 @@ class StoragePathRepositoryImpl
|
||||
@override
|
||||
Future<StoragePath> update(StoragePath storagePath) async {
|
||||
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));
|
||||
return updated;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class StoragePathRepositoryImpl
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson(StoragePathRepositoryState state) {
|
||||
Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:paperless_api/paperless_api.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/repository_state.dart';
|
||||
|
||||
class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
class TagRepositoryImpl extends LabelRepository<Tag> {
|
||||
final PaperlessLabelsApi _api;
|
||||
|
||||
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
|
||||
@@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
@override
|
||||
Future<Tag> create(Tag object) async {
|
||||
final created = await _api.saveTag(object);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..putIfAbsent(created.id!, () => created);
|
||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return created;
|
||||
@@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
@override
|
||||
Future<int> delete(Tag tag) async {
|
||||
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));
|
||||
return tag.id!;
|
||||
}
|
||||
@@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
Future<Tag?> find(int id) async {
|
||||
final tag = await _api.getTag(id);
|
||||
if (tag != null) {
|
||||
final updatedState = {...state.values}..[id] = tag;
|
||||
final updatedState = {...state.values ?? {}}..[id] = tag;
|
||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return tag;
|
||||
}
|
||||
@@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
@override
|
||||
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
|
||||
final tags = await _api.getTags(ids);
|
||||
final updatedState = {...state.values}
|
||||
final updatedState = {...state.values ?? {}}
|
||||
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||
return tags;
|
||||
@@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
@override
|
||||
Future<Tag> update(Tag tag) async {
|
||||
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));
|
||||
return updated;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(TagRepositoryState state) {
|
||||
Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:paperless_api/paperless_api.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>
|
||||
extends BaseRepository<State, T> {
|
||||
LabelRepository(State initial) : super(initial);
|
||||
abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
|
||||
LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
|
||||
}
|
||||
|
||||
@@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget {
|
||||
return MultiRepositoryProvider(
|
||||
providers: [
|
||||
RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
RepositoryProvider(
|
||||
create: (context) => context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||
),
|
||||
RepositoryProvider(
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:paperless_api/paperless_api.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/indexed_repository_state.dart';
|
||||
|
||||
abstract class SavedViewRepository
|
||||
extends BaseRepository<SavedViewRepositoryState, SavedView> {
|
||||
abstract class SavedViewRepository extends BaseRepository<SavedView> {
|
||||
SavedViewRepository(super.initialState);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:json_annotation/json_annotation.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';
|
||||
|
||||
@JsonSerializable()
|
||||
class CorrespondentRepositoryState
|
||||
extends RepositoryState<Map<int, Correspondent>> {
|
||||
extends IndexedRepositoryState<Correspondent> {
|
||||
const CorrespondentRepositoryState({
|
||||
super.values = const {},
|
||||
super.hasLoaded,
|
||||
|
||||
@@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson(
|
||||
Map<String, dynamic> _$CorrespondentRepositoryStateToJson(
|
||||
CorrespondentRepositoryState instance) =>
|
||||
<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,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
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';
|
||||
|
||||
part 'document_type_repository_state.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DocumentTypeRepositoryState
|
||||
extends RepositoryState<Map<int, DocumentType>> {
|
||||
class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
|
||||
const DocumentTypeRepositoryState({
|
||||
super.values = const {},
|
||||
super.hasLoaded,
|
||||
});
|
||||
|
||||
@override
|
||||
DocumentTypeRepositoryState copyWith(
|
||||
{Map<int, DocumentType>? values, bool? hasLoaded}) {
|
||||
DocumentTypeRepositoryState copyWith({
|
||||
Map<int, DocumentType>? values,
|
||||
bool? hasLoaded,
|
||||
}) {
|
||||
return DocumentTypeRepositoryState(
|
||||
values: values ?? this.values,
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
|
||||
@@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson(
|
||||
Map<String, dynamic> _$DocumentTypeRepositoryStateToJson(
|
||||
DocumentTypeRepositoryState instance) =>
|
||||
<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,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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';
|
||||
|
||||
part 'saved_view_repository_state.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class SavedViewRepositoryState extends RepositoryState<Map<int, SavedView>> {
|
||||
class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
|
||||
const SavedViewRepositoryState({
|
||||
super.values = const {},
|
||||
super.hasLoaded = false,
|
||||
|
||||
@@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
|
||||
Map<String, dynamic> _$SavedViewRepositoryStateToJson(
|
||||
SavedViewRepositoryState instance) =>
|
||||
<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,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
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';
|
||||
|
||||
part 'storage_path_repository_state.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class StoragePathRepositoryState
|
||||
extends RepositoryState<Map<int, StoragePath>> {
|
||||
class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
|
||||
const StoragePathRepositoryState({
|
||||
super.values = const {},
|
||||
super.hasLoaded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
StoragePathRepositoryState copyWith(
|
||||
{Map<int, StoragePath>? values, bool? hasLoaded}) {
|
||||
StoragePathRepositoryState copyWith({
|
||||
Map<int, StoragePath>? values,
|
||||
bool? hasLoaded,
|
||||
}) {
|
||||
return StoragePathRepositoryState(
|
||||
values: values ?? this.values,
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
|
||||
@@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson(
|
||||
Map<String, dynamic> _$StoragePathRepositoryStateToJson(
|
||||
StoragePathRepositoryState instance) =>
|
||||
<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,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:json_annotation/json_annotation.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';
|
||||
|
||||
@JsonSerializable()
|
||||
class TagRepositoryState extends RepositoryState<Map<int, Tag>> {
|
||||
class TagRepositoryState extends IndexedRepositoryState<Tag> {
|
||||
const TagRepositoryState({
|
||||
super.values = const {},
|
||||
super.hasLoaded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
TagRepositoryState copyWith({Map<int, Tag>? values, bool? hasLoaded}) {
|
||||
TagRepositoryState copyWith({
|
||||
Map<int, Tag>? values,
|
||||
bool? hasLoaded,
|
||||
}) {
|
||||
return TagRepositoryState(
|
||||
values: values ?? this.values,
|
||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||
|
||||
@@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) =>
|
||||
<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,
|
||||
};
|
||||
|
||||
16
lib/core/repository/state/indexed_repository_state.dart
Normal file
16
lib/core/repository/state/indexed_repository_state.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class GithubIssueService {
|
||||
..tryPutIfAbsent('assignees', () => assignees?.join(','))
|
||||
..tryPutIfAbsent('project', () => project),
|
||||
);
|
||||
log("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
|
||||
debugPrint("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
|
||||
launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Shows a full screen search page and returns the search result selected by
|
||||
/// the user when the page is closed.
|
||||
@@ -221,12 +222,13 @@ abstract class SearchDelegate<T> {
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
return theme.copyWith(
|
||||
appBarTheme: AppBarTheme(
|
||||
brightness: colorScheme.brightness,
|
||||
systemOverlayStyle: colorScheme.brightness == Brightness.light
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
backgroundColor: colorScheme.brightness == Brightness.dark
|
||||
? Colors.grey[900]
|
||||
: Colors.white,
|
||||
iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
|
||||
textTheme: theme.textTheme,
|
||||
),
|
||||
inputDecorationTheme: searchFieldDecorationTheme ??
|
||||
InputDecorationTheme(
|
||||
|
||||
21
lib/core/widgets/shimmer_placeholder.dart
Normal file
21
lib/core/widgets/shimmer_placeholder.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,32 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:open_filex/open_filex.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';
|
||||
|
||||
part 'document_details_state.dart';
|
||||
|
||||
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
final PaperlessDocumentsApi _api;
|
||||
final DocumentChangedNotifier _notifier;
|
||||
|
||||
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
|
||||
: super(DocumentDetailsState(document: initialDocument)) {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
DocumentDetailsCubit(
|
||||
this._api,
|
||||
this._notifier, {
|
||||
required DocumentModel initialDocument,
|
||||
}) : super(DocumentDetailsState(document: initialDocument)) {
|
||||
_notifier.subscribe(this, onUpdated: replace);
|
||||
loadSuggestions();
|
||||
}
|
||||
|
||||
Future<void> delete(DocumentModel document) async {
|
||||
await _api.delete(document);
|
||||
_notifier.notifyDeleted(document);
|
||||
}
|
||||
|
||||
Future<void> loadSuggestions() async {
|
||||
@@ -41,7 +50,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
final int asn = await _api.findNextAsn();
|
||||
final updatedDocument =
|
||||
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));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
for (final element in _subscriptions) {
|
||||
element.cancel();
|
||||
}
|
||||
_notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:badges/badges.dart' as b;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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:paperless_api/paperless_api.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/widgets/highlighted_text.dart';
|
||||
import 'package:paperless_mobile/core/widgets/offline_widget.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/similar_documents_view.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_view.dart';
|
||||
@@ -30,9 +28,7 @@ import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:badges/badges.dart' as b;
|
||||
|
||||
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
|
||||
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
|
||||
|
||||
//TODO: Refactor this into several widgets
|
||||
class DocumentDetailsPage extends StatefulWidget {
|
||||
@@ -79,16 +75,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
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,
|
||||
),
|
||||
),
|
||||
leading: const BackButton(),
|
||||
floating: true,
|
||||
pinned: true,
|
||||
expandedHeight: 200.0,
|
||||
@@ -153,6 +140,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
builder: (context, state) {
|
||||
return BlocProvider(
|
||||
create: (context) => SimilarDocumentsCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
documentId: state.document.id,
|
||||
),
|
||||
@@ -168,7 +156,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
_buildDocumentMetaDataView(
|
||||
state.document,
|
||||
),
|
||||
_buildSimilarDocumentsView(),
|
||||
const SimilarDocumentsView(),
|
||||
],
|
||||
),
|
||||
).paddedSymmetrically(horizontal: 8);
|
||||
@@ -284,6 +272,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
documentTypeRepository: context.read(),
|
||||
storagePathRepository: context.read(),
|
||||
tagRepository: context.read(),
|
||||
notifier: context.read(),
|
||||
),
|
||||
),
|
||||
BlocProvider<DocumentDetailsCubit>.value(
|
||||
@@ -294,7 +283,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
listenWhen: (previous, current) =>
|
||||
previous.document != current.document,
|
||||
listener: (context, state) {
|
||||
cubit.replaceDocument(state.document);
|
||||
cubit.replace(state.document);
|
||||
},
|
||||
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
@@ -461,7 +450,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
visible: document.documentType != null,
|
||||
child: _DetailsItem(
|
||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||
content: LabelText<DocumentType, DocumentTypeRepositoryState>(
|
||||
content: LabelText<DocumentType>(
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
id: document.documentType,
|
||||
),
|
||||
@@ -471,7 +460,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
visible: document.correspondent != null,
|
||||
child: _DetailsItem(
|
||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||
content: LabelText<Correspondent, CorrespondentRepositoryState>(
|
||||
content: LabelText<Correspondent>(
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
id: document.correspondent,
|
||||
),
|
||||
@@ -555,10 +544,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSimilarDocumentsView() {
|
||||
return const SimilarDocumentsView();
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailsItem extends StatelessWidget {
|
||||
|
||||
@@ -13,7 +13,13 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
final DocumentChangedNotifier 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 {
|
||||
emit(state.copyWith(
|
||||
@@ -61,6 +67,12 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
DocumentSearchState? fromJson(Map<String, dynamic> json) {
|
||||
return DocumentSearchState.fromJson(json);
|
||||
|
||||
@@ -158,18 +158,16 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
||||
isLabelClickable: false,
|
||||
isLoading: state.isLoading,
|
||||
hasLoaded: state.hasLoaded,
|
||||
onTap: (document) async {
|
||||
final updatedDocument = await Navigator.pushNamed(
|
||||
enableHeroAnimation: false,
|
||||
onTap: (document) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
document: document,
|
||||
isLabelClickable: false,
|
||||
),
|
||||
) as DocumentModel?;
|
||||
if (updatedDocument != document) {
|
||||
context.read<DocumentSearchCubit>().reload();
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
||||
@@ -14,21 +14,17 @@ part 'document_upload_state.dart';
|
||||
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
final PaperlessDocumentsApi _documentApi;
|
||||
|
||||
final LabelRepository<Tag, TagRepositoryState> _tagRepository;
|
||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
_correspondentRepository;
|
||||
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||
_documentTypeRepository;
|
||||
final LabelRepository<Tag> _tagRepository;
|
||||
final LabelRepository<Correspondent> _correspondentRepository;
|
||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||
|
||||
final List<StreamSubscription> _subs = [];
|
||||
|
||||
DocumentUploadCubit({
|
||||
required PaperlessDocumentsApi documentApi,
|
||||
required LabelRepository<Tag, TagRepositoryState> tagRepository,
|
||||
required LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
correspondentRepository,
|
||||
required LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||
documentTypeRepository,
|
||||
required LabelRepository<Tag> tagRepository,
|
||||
required LabelRepository<Correspondent> correspondentRepository,
|
||||
required LabelRepository<DocumentType> documentTypeRepository,
|
||||
}) : _documentApi = documentApi,
|
||||
_tagRepository = tagRepository,
|
||||
_correspondentRepository = correspondentRepository,
|
||||
|
||||
@@ -8,10 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.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/widgets/hint_card.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/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/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
|
||||
class DocumentUploadPreparationPage extends StatefulWidget {
|
||||
final Uint8List fileBytes;
|
||||
@@ -173,9 +169,8 @@ class _DocumentUploadPreparationPageState
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialName) =>
|
||||
RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
),
|
||||
textFieldLabel:
|
||||
@@ -189,9 +184,8 @@ class _DocumentUploadPreparationPageState
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialName) =>
|
||||
RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
),
|
||||
textFieldLabel:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||
|
||||
@@ -17,14 +17,21 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
|
||||
reload();
|
||||
notifier.subscribe(
|
||||
this,
|
||||
onUpdated: replace,
|
||||
onDeleted: remove,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> bulkRemove(List<DocumentModel> documents) async {
|
||||
log("[DocumentsCubit] bulkRemove");
|
||||
Future<void> bulkDelete(List<DocumentModel> documents) async {
|
||||
debugPrint("[DocumentsCubit] bulkRemove");
|
||||
await api.bulkAction(
|
||||
BulkDeleteAction(documents.map((doc) => doc.id)),
|
||||
);
|
||||
for (final deletedDoc in documents) {
|
||||
notifier.notifyDeleted(deletedDoc);
|
||||
}
|
||||
await reload();
|
||||
}
|
||||
|
||||
@@ -33,7 +40,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
Iterable<int> addTags = const [],
|
||||
Iterable<int> removeTags = const [],
|
||||
}) async {
|
||||
log("[DocumentsCubit] bulkEditTags");
|
||||
debugPrint("[DocumentsCubit] bulkEditTags");
|
||||
await api.bulkAction(BulkModifyTagsAction(
|
||||
documents.map((doc) => doc.id),
|
||||
addTags: addTags,
|
||||
@@ -43,7 +50,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
}
|
||||
|
||||
void toggleDocumentSelection(DocumentModel model) {
|
||||
log("[DocumentsCubit] toggleSelection");
|
||||
debugPrint("[DocumentsCubit] toggleSelection");
|
||||
if (state.selectedIds.contains(model.id)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -58,12 +65,12 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
}
|
||||
|
||||
void resetSelection() {
|
||||
log("[DocumentsCubit] resetSelection");
|
||||
debugPrint("[DocumentsCubit] resetSelection");
|
||||
emit(state.copyWith(selection: []));
|
||||
}
|
||||
|
||||
void reset() {
|
||||
log("[DocumentsCubit] reset");
|
||||
debugPrint("[DocumentsCubit] reset");
|
||||
emit(const DocumentsState());
|
||||
}
|
||||
|
||||
@@ -81,4 +88,10 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
Map<String, dynamic>? toJson(DocumentsState state) {
|
||||
return state.toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||
|
||||
class DocumentsState extends PagedDocumentsState {
|
||||
@JsonKey(ignore: true)
|
||||
@JsonKey(includeFromJson: true, includeToJson: false)
|
||||
final List<DocumentModel> selection;
|
||||
|
||||
const DocumentsState({
|
||||
@@ -34,11 +34,8 @@ class DocumentsState extends PagedDocumentsState {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
hasLoaded,
|
||||
filter,
|
||||
value,
|
||||
selection,
|
||||
isLoading,
|
||||
...super.props,
|
||||
];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
||||
@@ -160,8 +160,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||
child: AddStoragePathPage(initalValue: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||
@@ -182,8 +181,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||
child: AddCorrespondentPage(initialName: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||
@@ -215,8 +213,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
),
|
||||
|
||||
@@ -249,7 +249,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return RefreshIndicator(
|
||||
edgeOffset: kToolbarHeight,
|
||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||
onRefresh: _onReloadDocuments,
|
||||
notificationPredicate: (_) =>
|
||||
connectivityState.isConnected,
|
||||
@@ -263,13 +263,14 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
),
|
||||
_buildViewActions(),
|
||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
buildWhen: (previous, current) =>
|
||||
!const ListEquality().equals(
|
||||
previous.documents,
|
||||
current.documents,
|
||||
) ||
|
||||
previous.selectedIds !=
|
||||
current.selectedIds,
|
||||
// Not required anymore since saved views are now handled separately
|
||||
// buildWhen: (previous, current) =>
|
||||
// !const ListEquality().equals(
|
||||
// previous.documents,
|
||||
// current.documents,
|
||||
// ) ||
|
||||
// previous.selectedIds !=
|
||||
// current.selectedIds,
|
||||
builder: (context, state) {
|
||||
if (state.hasLoaded &&
|
||||
state.documents.isEmpty) {
|
||||
@@ -323,7 +324,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return RefreshIndicator(
|
||||
edgeOffset: kToolbarHeight,
|
||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||
onRefresh: _onReloadSavedViews,
|
||||
notificationPredicate: (_) =>
|
||||
connectivityState.isConnected,
|
||||
@@ -390,7 +391,7 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
try {
|
||||
await context
|
||||
.read<DocumentsCubit>()
|
||||
.bulkRemove(documentsState.selection);
|
||||
.bulkDelete(documentsState.selection);
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
||||
@@ -467,20 +468,14 @@ class _DocumentsPageState extends State<DocumentsPage>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openDetails(DocumentModel document) async {
|
||||
final updatedModel = await Navigator.pushNamed(
|
||||
void _openDetails(DocumentModel document) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
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) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.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/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_list_item.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)? onStoragePathSelected;
|
||||
|
||||
bool get showLoadingPlaceholder => (!hasLoaded && isLoading);
|
||||
const AdaptiveDocumentsView({
|
||||
super.key,
|
||||
this.selectedDocumentIds = const [],
|
||||
@@ -56,6 +57,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
super.onTap,
|
||||
super.selectedDocumentIds,
|
||||
super.viewType,
|
||||
super.enableHeroAnimation,
|
||||
required super.isLoading,
|
||||
required super.hasLoaded,
|
||||
});
|
||||
@@ -71,8 +73,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
Widget _buildListView() {
|
||||
if (!hasLoaded && isLoading) {
|
||||
return const DocumentsListLoadingWidget();
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentsListLoadingWidget.sliver();
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
@@ -91,6 +93,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
onCorrespondentSelected: onCorrespondentSelected,
|
||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||
onStoragePathSelected: onStoragePathSelected,
|
||||
enableHeroAnimation: enableHeroAnimation,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -99,8 +102,8 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
Widget _buildGridView() {
|
||||
if (!hasLoaded && isLoading) {
|
||||
return const DocumentsListLoadingWidget();
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentGridLoadingWidget.sliver();
|
||||
}
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -162,10 +165,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
Widget _buildListView() {
|
||||
if (!hasLoaded && isLoading) {
|
||||
return const CustomScrollView(slivers: [
|
||||
DocumentsListLoadingWidget(),
|
||||
]);
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentsListLoadingWidget();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
@@ -194,12 +195,8 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
}
|
||||
|
||||
Widget _buildGridView() {
|
||||
if (!hasLoaded && isLoading) {
|
||||
return const CustomScrollView(
|
||||
slivers: [
|
||||
DocumentsListLoadingWidget(),
|
||||
],
|
||||
); //TODO: Build grid skeleton
|
||||
if (showLoadingPlaceholder) {
|
||||
return DocumentGridLoadingWidget();
|
||||
}
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
|
||||
@@ -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!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.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 {
|
||||
static const _tags = [" ", " ", " "];
|
||||
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||
static const _correspondentLengths = <double>[200.0, 300.0, 150.0];
|
||||
static const _fontSize = 16.0;
|
||||
class DocumentsListLoadingWidget extends StatelessWidget
|
||||
with DocumentItemPlaceholder {
|
||||
final bool _isSliver;
|
||||
DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||
|
||||
const DocumentsListLoadingWidget({super.key
|
||||
});
|
||||
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||
|
||||
@override
|
||||
final Random random = Random(1209571050);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _random = Random();
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return _buildFakeListItem(context, _random);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (_isSliver) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildFakeListItem(context),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _buildFakeListItem(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFakeListItem(BuildContext context, Random random) {
|
||||
final tagCount = random.nextInt(_tags.length + 1);
|
||||
final correspondentLength =
|
||||
_correspondentLengths[random.nextInt(_correspondentLengths.length - 1)];
|
||||
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]!,
|
||||
Widget _buildFakeListItem(BuildContext context) {
|
||||
const fontSize = 14.0;
|
||||
final values = nextValues;
|
||||
return ShimmerPlaceholder(
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
dense: true,
|
||||
@@ -45,15 +45,17 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
height: 50,
|
||||
height: double.infinity,
|
||||
width: 35,
|
||||
),
|
||||
),
|
||||
title: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
width: correspondentLength,
|
||||
height: _fontSize,
|
||||
color: Colors.white,
|
||||
title: Row(
|
||||
children: [
|
||||
TextPlaceholder(
|
||||
length: values.correspondentLength,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
@@ -61,21 +63,16 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
height: _fontSize,
|
||||
width: titleLength,
|
||||
color: Colors.white,
|
||||
TextPlaceholder(
|
||||
length: values.titleLength,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -56,7 +56,7 @@ class DocumentListItem extends DocumentItem {
|
||||
Text(
|
||||
document.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: document.tags.isEmpty ? 2 : 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
AbsorbPointer(
|
||||
absorbing: isSelectionActive,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -44,16 +44,12 @@ class SortDocumentsButton extends StatelessWidget {
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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:collection/collection.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 PaperlessDocumentsApi _docsApi;
|
||||
|
||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
_correspondentRepository;
|
||||
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||
_documentTypeRepository;
|
||||
final LabelRepository<StoragePath, StoragePathRepositoryState>
|
||||
_storagePathRepository;
|
||||
final LabelRepository<Tag, TagRepositoryState> _tagRepository;
|
||||
|
||||
final DocumentChangedNotifier _notifier;
|
||||
final LabelRepository<Correspondent> _correspondentRepository;
|
||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||
final LabelRepository<StoragePath> _storagePathRepository;
|
||||
final LabelRepository<Tag> _tagRepository;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
EditDocumentCubit(
|
||||
DocumentModel document, {
|
||||
required PaperlessDocumentsApi documentsApi,
|
||||
required LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
correspondentRepository,
|
||||
required LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||
documentTypeRepository,
|
||||
required LabelRepository<StoragePath, StoragePathRepositoryState>
|
||||
storagePathRepository,
|
||||
required LabelRepository<Tag, TagRepositoryState> tagRepository,
|
||||
required LabelRepository<Correspondent> correspondentRepository,
|
||||
required LabelRepository<DocumentType> documentTypeRepository,
|
||||
required LabelRepository<StoragePath> storagePathRepository,
|
||||
required LabelRepository<Tag> tagRepository,
|
||||
required DocumentChangedNotifier notifier,
|
||||
}) : _initialDocument = document,
|
||||
_docsApi = documentsApi,
|
||||
_correspondentRepository = correspondentRepository,
|
||||
_documentTypeRepository = documentTypeRepository,
|
||||
_storagePathRepository = storagePathRepository,
|
||||
_tagRepository = tagRepository,
|
||||
_notifier = notifier,
|
||||
super(
|
||||
EditDocumentState(
|
||||
document: document,
|
||||
@@ -50,6 +48,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
||||
tags: tagRepository.current?.values ?? {},
|
||||
),
|
||||
) {
|
||||
_notifier.subscribe(this, onUpdated: replace);
|
||||
_subscriptions.add(
|
||||
_correspondentRepository.values
|
||||
.listen((v) => emit(state.copyWith(correspondents: v?.values))),
|
||||
@@ -71,6 +70,8 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
||||
|
||||
Future<void> updateDocument(DocumentModel document) async {
|
||||
final updated = await _docsApi.update(document);
|
||||
_notifier.notifyUpdated(updated);
|
||||
|
||||
// Reload changed labels (documentCount property changes with removal/add)
|
||||
if (document.documentType != _initialDocument.documentType) {
|
||||
_documentTypeRepository
|
||||
@@ -88,7 +89,10 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
||||
.equals(document.tags, _initialDocument.tags)) {
|
||||
_tagRepository.findAll(document.tags);
|
||||
}
|
||||
emit(state.copyWith(document: updated));
|
||||
}
|
||||
|
||||
void replace(DocumentModel document) {
|
||||
emit(state.copyWith(document: document));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -96,6 +100,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
||||
for (final sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
_notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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';
|
||||
|
||||
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
|
||||
final LabelRepository<T, RepositoryState<Map<int, T>>> _repository;
|
||||
final LabelRepository<T> _repository;
|
||||
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
EditLabelCubit(LabelRepository<T, RepositoryState<Map<int, T>>> repository)
|
||||
EditLabelCubit(LabelRepository<T> repository)
|
||||
: _repository = repository,
|
||||
super(const EditLabelInitial()) {
|
||||
_subscription = repository.values.listen(
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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/view/label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
@@ -25,8 +25,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit(
|
||||
context
|
||||
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
|
||||
context.read<LabelRepository<T>>(),
|
||||
),
|
||||
child: AddLabelFormWidget(
|
||||
pageTitle: pageTitle,
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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/view/label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
@@ -28,8 +28,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit(
|
||||
context
|
||||
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
|
||||
context.read<LabelRepository<T>>(),
|
||||
),
|
||||
child: EditLabelForm(
|
||||
label: label,
|
||||
|
||||
@@ -15,8 +15,7 @@ class AddCorrespondentPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
child: AddLabelPage<Correspondent>(
|
||||
pageTitle: Text(S.of(context).addCorrespondentPageTitle),
|
||||
|
||||
@@ -18,8 +18,7 @@ class AddDocumentTypePage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<DocumentType>(
|
||||
context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
child: AddLabelPage<DocumentType>(
|
||||
pageTitle: Text(S.of(context).addDocumentTypePageTitle),
|
||||
|
||||
@@ -16,8 +16,7 @@ class AddStoragePathPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<StoragePath>(
|
||||
context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
context.read<LabelRepository<StoragePath>>(),
|
||||
),
|
||||
child: AddLabelPage<StoragePath>(
|
||||
pageTitle: Text(S.of(context).addStoragePathPageTitle),
|
||||
|
||||
@@ -19,7 +19,7 @@ class AddTagPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<Tag>(
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
child: AddLabelPage<Tag>(
|
||||
pageTitle: Text(S.of(context).addTagPageTitle),
|
||||
|
||||
@@ -14,8 +14,7 @@ class EditCorrespondentPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
child: EditLabelPage<Correspondent>(
|
||||
label: correspondent,
|
||||
|
||||
@@ -14,8 +14,7 @@ class EditDocumentTypePage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<DocumentType>(
|
||||
context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
child: EditLabelPage<DocumentType>(
|
||||
label: documentType,
|
||||
|
||||
@@ -15,8 +15,7 @@ class EditStoragePathPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<StoragePath>(
|
||||
context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
context.read<LabelRepository<StoragePath>>(),
|
||||
),
|
||||
child: EditLabelPage<StoragePath>(
|
||||
label: storagePath,
|
||||
|
||||
@@ -18,7 +18,7 @@ class EditTagPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => EditLabelCubit<Tag>(
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
child: EditLabelPage<Tag>(
|
||||
label: tag,
|
||||
|
||||
@@ -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/state/inbox_state.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/notifications/services/local_notification_service.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<ConnectivityCubit>().reload();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
@@ -228,7 +230,23 @@ class _HomePageState extends State<HomePage> {
|
||||
value: _scannerCubit,
|
||||
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(
|
||||
value: _inboxCubit,
|
||||
child: const InboxPage(),
|
||||
@@ -302,16 +320,10 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
void _initializeData(BuildContext context) {
|
||||
try {
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>().findAll();
|
||||
context
|
||||
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
|
||||
.findAll();
|
||||
context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
|
||||
.findAll();
|
||||
context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
|
||||
.findAll();
|
||||
context.read<LabelRepository<Tag>>().findAll();
|
||||
context.read<LabelRepository<Correspondent>>().findAll();
|
||||
context.read<LabelRepository<DocumentType>>().findAll();
|
||||
context.read<LabelRepository<StoragePath>>().findAll();
|
||||
context.read<SavedViewRepository>().findAll();
|
||||
context.read<PaperlessServerInformationCubit>().updateInformtion();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
|
||||
@@ -70,16 +70,10 @@ class VerifyIdentityPage extends StatelessWidget {
|
||||
|
||||
void _logout(BuildContext context) {
|
||||
context.read<AuthenticationCubit>().logout();
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>().clear();
|
||||
context
|
||||
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
|
||||
.clear();
|
||||
context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
|
||||
.clear();
|
||||
context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
|
||||
.clear();
|
||||
context.read<LabelRepository<Tag>>().clear();
|
||||
context.read<LabelRepository<Correspondent>>().clear();
|
||||
context.read<LabelRepository<DocumentType>>().clear();
|
||||
context.read<LabelRepository<StoragePath>>().clear();
|
||||
context.read<SavedViewRepository>().clear();
|
||||
HydratedBloc.storage.clear();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/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/paged_document_view/paged_documents_mixin.dart';
|
||||
|
||||
class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
||||
_correspondentRepository;
|
||||
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
||||
_documentTypeRepository;
|
||||
final LabelRepository<Tag> _tagsRepository;
|
||||
final LabelRepository<Correspondent> _correspondentRepository;
|
||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||
|
||||
final PaperlessDocumentsApi _documentsApi;
|
||||
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
@@ -28,7 +25,6 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
@override
|
||||
PaperlessDocumentsApi get api => _documentsApi;
|
||||
|
||||
Timer? _taskTimer;
|
||||
InboxCubit(
|
||||
this._tagsRepository,
|
||||
this._documentsApi,
|
||||
@@ -45,11 +41,20 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
availableTags: _tagsRepository.current?.values ?? {},
|
||||
),
|
||||
) {
|
||||
_subscriptions.addAll(
|
||||
notifier.listen(
|
||||
onDeleted: remove,
|
||||
onUpdated: replace,
|
||||
),
|
||||
notifier.subscribe(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
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(
|
||||
_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();
|
||||
});
|
||||
}
|
||||
|
||||
void refreshItemsInInboxCount() async {
|
||||
void refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
||||
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).
|
||||
///
|
||||
Future<void> initializeInbox() async {
|
||||
Future<void> loadInbox() async {
|
||||
final inboxTags = await _tagsRepository.findAll().then(
|
||||
(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));
|
||||
return updateFilter(
|
||||
updateFilter(
|
||||
filter: DocumentFilter(
|
||||
sortField: SortField.added,
|
||||
tags: IdsTagsQuery.fromIds(inboxTags),
|
||||
@@ -121,11 +140,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
document.tags.toSet().intersection(state.inboxTags.toSet());
|
||||
|
||||
final updatedTags = {...document.tags}..removeAll(tagsToRemove);
|
||||
await api.update(
|
||||
final updatedDocument = await api.update(
|
||||
document.copyWith(tags: updatedTags),
|
||||
);
|
||||
await remove(document);
|
||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||
// Remove first so document is not replaced first.
|
||||
remove(document);
|
||||
notifier.notifyUpdated(updatedDocument);
|
||||
return tagsToRemove;
|
||||
}
|
||||
|
||||
@@ -136,10 +156,12 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
DocumentModel document,
|
||||
Iterable<int> removedTags,
|
||||
) async {
|
||||
final updatedDoc = document.copyWith(
|
||||
tags: {...document.tags, ...removedTags},
|
||||
final updatedDocument = await _documentsApi.update(
|
||||
document.copyWith(
|
||||
tags: {...document.tags, ...removedTags},
|
||||
),
|
||||
);
|
||||
await _documentsApi.update(updatedDoc);
|
||||
notifier.notifyUpdated(updatedDocument);
|
||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
|
||||
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 {
|
||||
if (document.archiveSerialNumber == null) {
|
||||
final int asn = await _documentsApi.findNextAsn();
|
||||
final updatedDocument = await _documentsApi
|
||||
.update(document.copyWith(archiveSerialNumber: asn));
|
||||
|
||||
replace(updatedDocument);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +214,6 @@ class InboxCubit extends HydratedCubit<InboxState> with PagedDocumentsMixin {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_taskTimer?.cancel();
|
||||
for (var sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'package:paperless_mobile/features/paged_document_view/model/paged_docume
|
||||
|
||||
part 'inbox_state.g.dart';
|
||||
|
||||
@JsonSerializable(
|
||||
ignoreUnannotated: true,
|
||||
)
|
||||
@JsonSerializable(ignoreUnannotated: true)
|
||||
class InboxState extends PagedDocumentsState {
|
||||
final Iterable<int> inboxTags;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class _InboxPageState extends State<InboxPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<InboxCubit>().initializeInbox();
|
||||
context.read<InboxCubit>().loadInbox();
|
||||
_scrollController.addListener(_listenForLoadNewData);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ class _InboxPageState extends State<InboxPage> {
|
||||
|
||||
@override
|
||||
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(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
||||
@@ -76,97 +82,105 @@ class _InboxPageState extends State<InboxPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
edgeOffset: 78,
|
||||
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SearchAppBar(
|
||||
hintText: S.of(context).documentSearchSearchDocuments,
|
||||
onOpenSearch: showDocumentSearchPage,
|
||||
),
|
||||
],
|
||||
body: BlocBuilder<InboxCubit, InboxState>(
|
||||
builder: (context, state) {
|
||||
if (!state.hasLoaded) {
|
||||
return const CustomScrollView(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
slivers: [DocumentsListLoadingWidget()],
|
||||
);
|
||||
}
|
||||
|
||||
if (state.documents.isEmpty) {
|
||||
return InboxEmptyWidget(
|
||||
emptyStateRefreshIndicatorKey: _emptyStateRefreshIndicatorKey,
|
||||
);
|
||||
}
|
||||
|
||||
// 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],
|
||||
);
|
||||
},
|
||||
body: BlocBuilder<InboxCubit, InboxState>(
|
||||
builder: (context, state) {
|
||||
return SafeArea(
|
||||
top: true,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.flattened
|
||||
.toList()
|
||||
..add(const SliverToBoxAdapter(child: SizedBox(height: 78)));
|
||||
// edgeOffset: kToolbarHeight,
|
||||
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],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.flattened
|
||||
.toList()
|
||||
..add(const SliverToBoxAdapter(child: SizedBox(height: 78)));
|
||||
// edgeOffset: kToolbarHeight,
|
||||
|
||||
return CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
show: !state.isHintAcknowledged,
|
||||
hintText: S.of(context).inboxPageUsageHintText,
|
||||
onHintAcknowledged: () =>
|
||||
context.read<InboxCubit>().acknowledgeHint(),
|
||||
),
|
||||
return RefreshIndicator(
|
||||
edgeOffset: kToolbarHeight,
|
||||
onRefresh: context.read<InboxCubit>().reload,
|
||||
child: CustomScrollView(
|
||||
physics: state.documents.isEmpty
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
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(),
|
||||
confirmDismiss: (_) => _onItemDismissed(doc),
|
||||
key: UniqueKey(),
|
||||
child: InboxItem(
|
||||
document: doc,
|
||||
onDocumentUpdated: (document) {
|
||||
context.read<InboxCubit>().replaceUpdatedDocument(document);
|
||||
},
|
||||
),
|
||||
child: InboxItem(document: doc),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class InboxEmptyWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
key: _emptyStateRefreshIndicatorKey,
|
||||
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
||||
onRefresh: () => context.read<InboxCubit>().loadInbox(),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/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/document_preview.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 {
|
||||
static const _a4AspectRatio = 1 / 1.4142;
|
||||
|
||||
final void Function(DocumentModel model) onDocumentUpdated;
|
||||
final DocumentModel document;
|
||||
const InboxItem({
|
||||
super.key,
|
||||
required this.document,
|
||||
required this.onDocumentUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -41,17 +34,14 @@ class _InboxItemState extends State<InboxItem> {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
final updatedDocument = await Navigator.pushNamed(
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
document: widget.document,
|
||||
isLabelClickable: false,
|
||||
),
|
||||
) as DocumentModel?;
|
||||
if (updatedDocument != null) {
|
||||
widget.onDocumentUpdated(updatedDocument);
|
||||
}
|
||||
);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
@@ -104,12 +94,12 @@ class _InboxItemState extends State<InboxItem> {
|
||||
);
|
||||
final actions = [
|
||||
_buildAssignAsnAction(chipShape, context),
|
||||
const SizedBox(width: 4.0),
|
||||
const SizedBox(width: 8.0),
|
||||
ColoredChipWrapper(
|
||||
child: ActionChip(
|
||||
avatar: const Icon(Icons.delete_outline),
|
||||
shape: chipShape,
|
||||
label: const Text("Delete document"),
|
||||
label: Text(S.of(context).inboxActionDeleteDocument),
|
||||
onPressed: () async {
|
||||
final shouldDelete = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -124,6 +114,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// return FutureBuilder<FieldSuggestions>(
|
||||
// future: _fieldSuggestions,
|
||||
// builder: (context, snapshot) {
|
||||
@@ -151,12 +142,14 @@ class _InboxItemState extends State<InboxItem> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.bolt_outlined),
|
||||
SizedBox(
|
||||
width: 40,
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 50,
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).inboxPageQuickActionsLabel,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
@@ -199,7 +192,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
? Text(
|
||||
'${S.of(context).documentArchiveSerialNumberPropertyShortLabel} #${widget.document.archiveSerialNumber}',
|
||||
)
|
||||
: const Text("Assign ASN"),
|
||||
: Text(S.of(context).inboxActionAssignAsn),
|
||||
onPressed: !hasAsn
|
||||
? () {
|
||||
setState(() {
|
||||
@@ -233,7 +226,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
Icons.description_outlined,
|
||||
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
|
||||
),
|
||||
LabelText<DocumentType, DocumentTypeRepositoryState>(
|
||||
LabelText<DocumentType>(
|
||||
id: widget.document.documentType,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
placeholder: "-",
|
||||
@@ -247,7 +240,7 @@ class _InboxItemState extends State<InboxItem> {
|
||||
Icons.person_outline,
|
||||
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
|
||||
),
|
||||
LabelText<Correspondent, CorrespondentRepositoryState>(
|
||||
LabelText<Correspondent>(
|
||||
id: widget.document.correspondent,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
placeholder: "-",
|
||||
|
||||
@@ -3,15 +3,14 @@ import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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';
|
||||
|
||||
class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
|
||||
final LabelRepository<T, RepositoryState> _repository;
|
||||
final LabelRepository<T> _repository;
|
||||
|
||||
late StreamSubscription _subscription;
|
||||
|
||||
LabelCubit(LabelRepository<T, RepositoryState> repository)
|
||||
LabelCubit(LabelRepository<T> repository)
|
||||
: _repository = repository,
|
||||
super(LabelState(
|
||||
isLoaded: repository.isInitialized,
|
||||
@@ -22,7 +21,8 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
|
||||
if (event == null) {
|
||||
emit(LabelState());
|
||||
}
|
||||
emit(LabelState(isLoaded: true, labels: event!.values));
|
||||
emit(
|
||||
LabelState(isLoaded: event!.hasLoaded, labels: event.values ?? {}));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
|
||||
class CorrespondentBlocProvider extends StatelessWidget {
|
||||
final Widget child;
|
||||
const CorrespondentBlocProvider({super.key, required this.child});
|
||||
const CorrespondentBlocProvider({
|
||||
super.key,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -13,8 +13,7 @@ class DocumentTypeBlocProvider extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -18,25 +18,22 @@ class LabelsBlocProvider extends StatelessWidget {
|
||||
providers: [
|
||||
BlocProvider<LabelCubit<StoragePath>>(
|
||||
create: (context) => LabelCubit<StoragePath>(
|
||||
context.read<
|
||||
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
context.read<LabelRepository<StoragePath>>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<LabelCubit<Correspondent>>(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<LabelCubit<DocumentType>>(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<LabelCubit<Tag>>(
|
||||
create: (context) => LabelCubit<Tag>(
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -13,8 +13,7 @@ class StoragePathBlocProvider extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<StoragePath>(
|
||||
context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
context.read<LabelRepository<StoragePath>>(),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ class TagBlocProvider extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<Tag>(
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -241,8 +241,7 @@ class _TagFormFieldState extends State<TagFormField> {
|
||||
final Tag? tag = await Navigator.of(context).push<Tag>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Tag>>(),
|
||||
child: AddTagPage(initialValue: _textEditingController.text),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -82,7 +82,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
),
|
||||
sliver: SearchAppBar(
|
||||
hintText: "Search documents", //TODO: INTL
|
||||
hintText: S.of(context).documentSearchSearchDocuments,
|
||||
onOpenSearch: showDocumentSearchPage,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
@@ -141,176 +141,138 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Correspondent>(
|
||||
context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
child: RefreshIndicator(
|
||||
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<DocumentType>(
|
||||
context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<Tag>(
|
||||
context
|
||||
.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => LabelCubit<StoragePath>(
|
||||
context.read<
|
||||
LabelRepository<StoragePath,
|
||||
StoragePathRepositoryState>>(),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
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,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||
child: EditCorrespondentPage(correspondent: correspondent),
|
||||
),
|
||||
),
|
||||
@@ -339,8 +300,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||
child: EditDocumentTypePage(documentType: docType),
|
||||
),
|
||||
),
|
||||
@@ -352,8 +312,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Tag>>(),
|
||||
child: EditTagPage(tag: tag),
|
||||
),
|
||||
),
|
||||
@@ -365,8 +324,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||
child: EditStoragePathPage(
|
||||
storagePath: path,
|
||||
),
|
||||
@@ -380,8 +338,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||
child: const AddCorrespondentPage(),
|
||||
),
|
||||
),
|
||||
@@ -393,8 +350,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||
child: const AddDocumentTypePage(),
|
||||
),
|
||||
),
|
||||
@@ -406,8 +362,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) =>
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<Tag>>(),
|
||||
child: const AddTagPage(),
|
||||
),
|
||||
),
|
||||
@@ -419,8 +374,7 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => RepositoryProvider(
|
||||
create: (context) => context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||
child: const AddStoragePathPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -48,8 +48,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => LinkedDocumentsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
filter,
|
||||
context.read(),
|
||||
context.read(),
|
||||
),
|
||||
child: const LinkedDocumentsPage(),
|
||||
),
|
||||
|
||||
@@ -37,60 +37,65 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivityState) {
|
||||
return BlocBuilder<LabelCubit<T>, LabelState<T>>(
|
||||
builder: (context, state) {
|
||||
if (!state.isLoaded && !connectivityState.isConnected) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
final labels = state.labels.values.toList()..sort();
|
||||
if (labels.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
emptyStateDescription,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onAddNew,
|
||||
child: Text(emptyStateActionButtonLabel),
|
||||
),
|
||||
].padded(),
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<T>(
|
||||
context.read(),
|
||||
),
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivityState) {
|
||||
return BlocBuilder<LabelCubit<T>, LabelState<T>>(
|
||||
builder: (context, state) {
|
||||
if (!state.isLoaded && !connectivityState.isConnected) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
final labels = state.labels.values.toList()..sort();
|
||||
if (labels.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
emptyStateDescription,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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_state.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
|
||||
|
||||
class LabelText<T extends Label, State extends RepositoryState>
|
||||
extends StatelessWidget {
|
||||
class LabelText<T extends Label> extends StatelessWidget {
|
||||
final int? id;
|
||||
final String placeholder;
|
||||
final TextStyle? style;
|
||||
@@ -24,7 +21,7 @@ class LabelText<T extends Label, State extends RepositoryState>
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LabelCubit<T>(
|
||||
context.read<LabelRepository<T, State>>(),
|
||||
context.read<LabelRepository<T>>(),
|
||||
),
|
||||
child: BlocBuilder<LabelCubit<T>, LabelState<T>>(
|
||||
builder: (context, state) {
|
||||
|
||||
@@ -11,12 +11,27 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState>
|
||||
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
|
||||
LinkedDocumentsCubit(
|
||||
this.api,
|
||||
DocumentFilter filter,
|
||||
this.api,
|
||||
this.notifier,
|
||||
) : super(const LinkedDocumentsState()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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/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/state/linked_documents_state.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
@@ -60,18 +56,15 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||
isLabelClickable: false,
|
||||
isLoading: state.isLoading,
|
||||
hasLoaded: state.hasLoaded,
|
||||
onTap: (document) async {
|
||||
final updatedDocument = await Navigator.pushNamed(
|
||||
onTap: (document) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
document: document,
|
||||
isLabelClickable: false,
|
||||
),
|
||||
) as DocumentModel?;
|
||||
if (updatedDocument != document) {
|
||||
context.read<LinkedDocumentsCubit>().reload();
|
||||
}
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:paperless_api/paperless_api.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) {
|
||||
log("Received Notification: ${response.payload}");
|
||||
debugPrint("Received Notification: ${response.payload}");
|
||||
if (response.notificationResponseType ==
|
||||
NotificationResponseType.selectedNotificationAction) {
|
||||
final action =
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -73,14 +75,18 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
|
||||
try {
|
||||
final filter = state.filter.copyWith(page: 1);
|
||||
final result = await api.findAll(filter);
|
||||
emit(state.copyWithPaged(
|
||||
hasLoaded: true,
|
||||
value: [result],
|
||||
isLoading: false,
|
||||
filter: filter,
|
||||
));
|
||||
if (!isClosed) {
|
||||
emit(state.copyWithPaged(
|
||||
hasLoaded: true,
|
||||
value: [result],
|
||||
isLoading: false,
|
||||
filter: filter,
|
||||
));
|
||||
}
|
||||
} 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
|
||||
/// replace the currently loaded one, otherwise all documents will be reloaded.
|
||||
///
|
||||
Future<void> update(
|
||||
DocumentModel document, {
|
||||
bool shouldReload = true,
|
||||
}) async {
|
||||
Future<void> update(DocumentModel document) async {
|
||||
final updatedDocument = await api.update(document);
|
||||
if (shouldReload) {
|
||||
await reload();
|
||||
} else {
|
||||
replace(updatedDocument);
|
||||
}
|
||||
notifier.notifyUpdated(updatedDocument);
|
||||
// replace(updatedDocument);
|
||||
}
|
||||
|
||||
///
|
||||
@@ -107,7 +107,8 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
|
||||
emit(state.copyWithPaged(isLoading: true));
|
||||
try {
|
||||
await api.delete(document);
|
||||
await remove(document);
|
||||
notifier.notifyDeleted(document);
|
||||
// remove(document); // Removing deleted now works with the change notifier.
|
||||
} finally {
|
||||
emit(state.copyWithPaged(isLoading: false));
|
||||
}
|
||||
@@ -117,7 +118,7 @@ mixin PagedDocumentsMixin<State extends PagedDocumentsState>
|
||||
/// Removes [document] from the currently loaded state.
|
||||
/// Does not delete it from the server!
|
||||
///
|
||||
Future<void> remove(DocumentModel document) async {
|
||||
void remove(DocumentModel document) {
|
||||
final index = state.value.indexWhere(
|
||||
(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
|
||||
/// loaded state.
|
||||
/// loaded state if the document's properties still match the given filter criteria, otherwise removes it.
|
||||
///
|
||||
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),
|
||||
);
|
||||
if (index != -1) {
|
||||
final foundPage = state.value[index];
|
||||
if (pageIndex != -1) {
|
||||
final foundPage = state.value[pageIndex];
|
||||
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
|
||||
.mapIndexed((currIndex, element) =>
|
||||
currIndex == index ? replacementPage : element)
|
||||
currIndex == pageIndex ? replacementPage : element)
|
||||
.toList(),
|
||||
));
|
||||
);
|
||||
emit(newState);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
notifier.unsubscribe(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
Future<void> initialize() async {
|
||||
final views = await _repository.findAll();
|
||||
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();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:bloc/bloc.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/paged_documents_mixin.dart';
|
||||
|
||||
@@ -10,11 +11,20 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
final SavedView savedView;
|
||||
SavedViewDetailsCubit(
|
||||
this.api, {
|
||||
this.api,
|
||||
this.notifier, {
|
||||
required this.savedView,
|
||||
}) : super(const SavedViewDetailsState()) {
|
||||
notifier.subscribe(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
onUpdated: replace,
|
||||
);
|
||||
updateFilter(filter: savedView.toDocumentFilter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class SavedViewList extends StatelessWidget {
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SavedViewDetailsCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
savedView: view,
|
||||
),
|
||||
|
||||
@@ -117,18 +117,14 @@ class _SavedViewPageState extends State<SavedViewPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onOpenDocumentDetails(DocumentModel document) async {
|
||||
final updatedDocument = await Navigator.pushNamed(
|
||||
void _onOpenDocumentDetails(DocumentModel document) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
document: document,
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/repository/label_repository.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/widgets/offline_banner.dart';
|
||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||
@@ -46,6 +43,15 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
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>(
|
||||
builder: (context, connectedState) {
|
||||
return Scaffold(
|
||||
@@ -61,7 +67,33 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
// ),
|
||||
body: BlocBuilder<DocumentScannerCubit, List<File>>(
|
||||
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,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SearchAppBar(
|
||||
@@ -76,8 +108,9 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
if (state.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: _buildEmptyState(connectedState.isConnected),
|
||||
SliverFillViewport(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
[_buildEmptyState(connectedState.isConnected)]),
|
||||
)
|
||||
else
|
||||
_buildImageGrid(state)
|
||||
@@ -229,13 +262,11 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentUploadCubit(
|
||||
documentApi: context.read<PaperlessDocumentsApi>(),
|
||||
correspondentRepository: context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
documentTypeRepository: context.read<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
tagRepository:
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
correspondentRepository:
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
documentTypeRepository:
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
tagRepository: context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
child: DocumentUploadPreparationPage(
|
||||
fileBytes: file.bytes,
|
||||
@@ -346,14 +377,11 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
child: BlocProvider(
|
||||
create: (context) => DocumentUploadCubit(
|
||||
documentApi: context.read<PaperlessDocumentsApi>(),
|
||||
correspondentRepository: context.read<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>(),
|
||||
documentTypeRepository: context.read<
|
||||
LabelRepository<DocumentType,
|
||||
DocumentTypeRepositoryState>>(),
|
||||
tagRepository:
|
||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||
correspondentRepository:
|
||||
context.read<LabelRepository<Correspondent>>(),
|
||||
documentTypeRepository:
|
||||
context.read<LabelRepository<DocumentType>>(),
|
||||
tagRepository: context.read<LabelRepository<Tag>>(),
|
||||
),
|
||||
child: DocumentUploadPreparationPage(
|
||||
fileBytes: file.readAsBytesSync(),
|
||||
|
||||
@@ -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/repository/label_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/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
@@ -26,9 +22,10 @@ class AccountSettingsDialog extends StatelessWidget {
|
||||
scrollable: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const CloseButton(),
|
||||
Text(S.of(context).accountSettingsTitle),
|
||||
const CloseButton(),
|
||||
],
|
||||
),
|
||||
content: BlocBuilder<PaperlessServerInformationCubit,
|
||||
@@ -55,7 +52,7 @@ class AccountSettingsDialog extends StatelessWidget {
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.person_add_rounded),
|
||||
title: const Text("Add another account"), //TODO: INTL
|
||||
title: Text(S.of(context).accountSettingsAddAnotherAccount),
|
||||
onTap: () {},
|
||||
),
|
||||
Divider(),
|
||||
@@ -87,16 +84,10 @@ class AccountSettingsDialog extends StatelessWidget {
|
||||
try {
|
||||
await context.read<AuthenticationCubit>().logout();
|
||||
await context.read<ApplicationSettingsCubit>().clear();
|
||||
await context.read<LabelRepository<Tag, TagRepositoryState>>().clear();
|
||||
await context
|
||||
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
|
||||
.clear();
|
||||
await context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
|
||||
.clear();
|
||||
await context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
|
||||
.clear();
|
||||
await context.read<LabelRepository<Tag>>().clear();
|
||||
await context.read<LabelRepository<Correspondent>>().clear();
|
||||
await context.read<LabelRepository<DocumentType>>().clear();
|
||||
await context.read<LabelRepository<StoragePath>>().clear();
|
||||
await context.read<SavedViewRepository>().clear();
|
||||
await HydratedBloc.storage.clear();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import 'package:bloc/bloc.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/model/paged_documents_state.dart';
|
||||
|
||||
part 'similar_documents_state.dart';
|
||||
|
||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||
with PagedDocumentsMixin<SimilarDocumentsState> {
|
||||
with PagedDocumentsMixin {
|
||||
final int documentId;
|
||||
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
SimilarDocumentsCubit(
|
||||
this.api, {
|
||||
this.api,
|
||||
this.notifier, {
|
||||
required this.documentId,
|
||||
}) : super(const SimilarDocumentsState());
|
||||
}) : super(const SimilarDocumentsState()) {
|
||||
notifier.subscribe(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
onUpdated: replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!state.hasLoaded) {
|
||||
|
||||
@@ -2,14 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.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/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/items/document_list_item.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/constants.dart';
|
||||
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||
|
||||
class SimilarDocumentsView extends StatefulWidget {
|
||||
const SimilarDocumentsView({super.key});
|
||||
@@ -54,13 +53,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const earlyPreviewHintCard = HintCard(
|
||||
hintIcon: Icons.construction,
|
||||
hintText: "This view is still work in progress.",
|
||||
);
|
||||
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.documents.isEmpty) {
|
||||
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
|
||||
return DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () => context.read<SimilarDocumentsCubit>().updateFilter(
|
||||
@@ -77,26 +72,23 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView> {
|
||||
return CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(child: earlyPreviewHintCard),
|
||||
SliverAdaptiveDocumentsView(
|
||||
documents: state.documents,
|
||||
hasInternetConnection: connectivity.isConnected,
|
||||
isLabelClickable: false,
|
||||
isLoading: state.isLoading,
|
||||
hasLoaded: state.hasLoaded,
|
||||
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: state.documents.length,
|
||||
(context, index) => DocumentListItem(
|
||||
document: state.documents[index],
|
||||
enableHeroAnimation: false,
|
||||
isLabelClickable: false,
|
||||
isSelected: false,
|
||||
isSelectionActive: false,
|
||||
),
|
||||
),
|
||||
enableHeroAnimation: false,
|
||||
onTap: (document) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
DocumentDetailsRoute.routeName,
|
||||
arguments: DocumentDetailsRouteArguments(
|
||||
document: document,
|
||||
isLabelClickable: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -6,6 +6,8 @@
|
||||
"name": {}
|
||||
}
|
||||
},
|
||||
"accountSettingsAddAnotherAccount": "Add another account",
|
||||
"@accountSettingsAddAnotherAccount": {},
|
||||
"accountSettingsTitle": "Account",
|
||||
"@accountSettingsTitle": {},
|
||||
"addCorrespondentPageTitle": "Nový korespondent",
|
||||
@@ -396,6 +398,10 @@
|
||||
"@genericActionUploadLabel": {},
|
||||
"genericMessageOfflineText": "Jste offline.",
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxActionAssignAsn": "Assign ASN",
|
||||
"@inboxActionAssignAsn": {},
|
||||
"inboxActionDeleteDocument": "Delete document",
|
||||
"@inboxActionDeleteDocument": {},
|
||||
"inboxPageAssignAsnLabel": "Assign ASN",
|
||||
"@inboxPageAssignAsnLabel": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Dokument odstraněn z inboxu.",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"name": {}
|
||||
}
|
||||
},
|
||||
"accountSettingsAddAnotherAccount": "Einen Account hinzufügen",
|
||||
"@accountSettingsAddAnotherAccount": {},
|
||||
"accountSettingsTitle": "Account",
|
||||
"@accountSettingsTitle": {},
|
||||
"addCorrespondentPageTitle": "Neuer Korrespondent",
|
||||
@@ -396,6 +398,10 @@
|
||||
"@genericActionUploadLabel": {},
|
||||
"genericMessageOfflineText": "Du bist offline.",
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxActionAssignAsn": "ASN zuweisen",
|
||||
"@inboxActionAssignAsn": {},
|
||||
"inboxActionDeleteDocument": "Dokument löschen",
|
||||
"@inboxActionDeleteDocument": {},
|
||||
"inboxPageAssignAsnLabel": "ASN zuweisen",
|
||||
"@inboxPageAssignAsnLabel": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"name": {}
|
||||
}
|
||||
},
|
||||
"accountSettingsAddAnotherAccount": "Add another account",
|
||||
"@accountSettingsAddAnotherAccount": {},
|
||||
"accountSettingsTitle": "Account",
|
||||
"@accountSettingsTitle": {},
|
||||
"addCorrespondentPageTitle": "New Correspondent",
|
||||
@@ -396,6 +398,10 @@
|
||||
"@genericActionUploadLabel": {},
|
||||
"genericMessageOfflineText": "You're offline.",
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxActionAssignAsn": "Assign ASN",
|
||||
"@inboxActionAssignAsn": {},
|
||||
"inboxActionDeleteDocument": "Delete document",
|
||||
"@inboxActionDeleteDocument": {},
|
||||
"inboxPageAssignAsnLabel": "Assign ASN",
|
||||
"@inboxPageAssignAsnLabel": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"name": {}
|
||||
}
|
||||
},
|
||||
"accountSettingsAddAnotherAccount": "Add another account",
|
||||
"@accountSettingsAddAnotherAccount": {},
|
||||
"accountSettingsTitle": "Account",
|
||||
"@accountSettingsTitle": {},
|
||||
"addCorrespondentPageTitle": "New Correspondent",
|
||||
@@ -396,6 +398,10 @@
|
||||
"@genericActionUploadLabel": {},
|
||||
"genericMessageOfflineText": "Jesteście w trybie offline.",
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxActionAssignAsn": "Assign ASN",
|
||||
"@inboxActionAssignAsn": {},
|
||||
"inboxActionDeleteDocument": "Delete document",
|
||||
"@inboxActionDeleteDocument": {},
|
||||
"inboxPageAssignAsnLabel": "Assign ASN",
|
||||
"@inboxPageAssignAsnLabel": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Dokument usunięty ze skrzynki odbiorczej",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"name": {}
|
||||
}
|
||||
},
|
||||
"accountSettingsAddAnotherAccount": "Add another account",
|
||||
"@accountSettingsAddAnotherAccount": {},
|
||||
"accountSettingsTitle": "Account",
|
||||
"@accountSettingsTitle": {},
|
||||
"addCorrespondentPageTitle": "Yeni ek yazar",
|
||||
@@ -396,6 +398,10 @@
|
||||
"@genericActionUploadLabel": {},
|
||||
"genericMessageOfflineText": "Çevrimdışısınız.",
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxActionAssignAsn": "Assign ASN",
|
||||
"@inboxActionAssignAsn": {},
|
||||
"inboxActionDeleteDocument": "Delete document",
|
||||
"@inboxActionDeleteDocument": {},
|
||||
"inboxPageAssignAsnLabel": "ASN ata",
|
||||
"@inboxPageAssignAsnLabel": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Döküman gelen kutusundan kaldırıldı.",
|
||||
|
||||
@@ -109,7 +109,7 @@ void main() async {
|
||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
||||
// Remove temporarily downloaded files.
|
||||
|
||||
(await FileService.temporaryDirectory).deleteSync(recursive: true);
|
||||
// (await FileService.temporaryDirectory).deleteSync(recursive: true);
|
||||
// Load application settings and stored authentication data
|
||||
await connectivityCubit.initialize();
|
||||
|
||||
@@ -173,20 +173,16 @@ void main() async {
|
||||
],
|
||||
child: MultiRepositoryProvider(
|
||||
providers: [
|
||||
RepositoryProvider<LabelRepository<Tag, TagRepositoryState>>.value(
|
||||
RepositoryProvider<LabelRepository<Tag>>.value(
|
||||
value: tagRepository,
|
||||
),
|
||||
RepositoryProvider<
|
||||
LabelRepository<Correspondent,
|
||||
CorrespondentRepositoryState>>.value(
|
||||
RepositoryProvider<LabelRepository<Correspondent>>.value(
|
||||
value: correspondentRepository,
|
||||
),
|
||||
RepositoryProvider<
|
||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>.value(
|
||||
RepositoryProvider<LabelRepository<DocumentType>>.value(
|
||||
value: documentTypeRepository,
|
||||
),
|
||||
RepositoryProvider<
|
||||
LabelRepository<StoragePath, StoragePathRepositoryState>>.value(
|
||||
RepositoryProvider<LabelRepository<StoragePath>>.value(
|
||||
value: storagePathRepository,
|
||||
),
|
||||
RepositoryProvider<SavedViewRepository>.value(
|
||||
|
||||
@@ -17,8 +17,9 @@ class DocumentDetailsRoute extends StatelessWidget {
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => DocumentDetailsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
args.document,
|
||||
context.read(),
|
||||
context.read(),
|
||||
initialDocument: args.document,
|
||||
),
|
||||
child: LabelRepositoriesProvider(
|
||||
child: DocumentDetailsPage(
|
||||
|
||||
@@ -146,7 +146,20 @@ class DocumentFilter extends Equatable {
|
||||
///
|
||||
/// 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 => [
|
||||
documentType != initial.documentType,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/src/models/document_model.dart';
|
||||
|
||||
const pageRegex = r".*page=(\d+).*";
|
||||
|
||||
@@ -108,5 +107,10 @@ class PagedSearchResult<T> extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [count, next, previous, results];
|
||||
List<Object?> get props => [
|
||||
count,
|
||||
next,
|
||||
previous,
|
||||
results,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -52,4 +52,17 @@ class AbsoluteDateRangeQuery extends DateRangeQuery {
|
||||
|
||||
@override
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ abstract class DateRangeQuery extends Equatable {
|
||||
Map<String, String> toQueryParameter(DateRangeQueryField field);
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
bool matches(DateTime dt);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user