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

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

View File

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

View File

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

View File

@@ -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 = (
);

View File

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

View File

@@ -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() {

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package: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();
}
}

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -1,23 +1,32 @@
import 'dart:async';
import 'dart:io';
import '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();
}
}

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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,

View File

@@ -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:

View File

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

View File

@@ -3,7 +3,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
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() {

View File

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

View File

@@ -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) {

View File

@@ -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,

View File

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

View File

@@ -1,42 +1,42 @@
import 'dart:math';
import '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),
],
),
),

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -44,16 +44,12 @@ class SortDocumentsButton extends StatelessWidget {
providers: [
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>>(),
),
),
],

View File

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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -23,6 +23,7 @@ import 'package:paperless_mobile/features/home/view/route_description.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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: "-",

View File

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

View File

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

View File

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

View File

@@ -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>>(),
),
),
],

View File

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

View File

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

View File

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

View File

@@ -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(),
),
),

View File

@@ -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(),
),

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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 =

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -12,9 +12,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/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(),

View File

@@ -6,10 +6,6 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/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) {

View File

@@ -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) {

View File

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

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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ı.",

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,

View File

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

View File

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

View File

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