mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-14 14:12:21 -06:00
Merge branch 'development'
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ android/key.properties
|
|||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
.vscode/
|
.vscode/
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ android {
|
|||||||
storePassword keystoreProperties['storePassword']
|
storePassword keystoreProperties['storePassword']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ PODS:
|
|||||||
- DKPhotoGallery/Resource (0.0.17):
|
- DKPhotoGallery/Resource (0.0.17):
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
- edge_detection (1.0.9):
|
- edge_detection (1.1.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WeScan
|
- WeScan
|
||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
@@ -44,6 +44,8 @@ PODS:
|
|||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_keyboard_visibility (0.0.1):
|
- flutter_keyboard_visibility (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_local_notifications (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_native_splash (0.0.1):
|
- flutter_native_splash (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
@@ -56,10 +58,13 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- local_auth_ios (0.0.1):
|
- local_auth_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- open_filex (0.0.2):
|
||||||
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- pdfx (1.0.0):
|
- pdfx (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- permission_handler_apple (9.0.4):
|
- permission_handler_apple (9.0.4):
|
||||||
@@ -72,8 +77,9 @@ PODS:
|
|||||||
- SDWebImage/Core (5.13.5)
|
- SDWebImage/Core (5.13.5)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_ios (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- sqflite (0.0.2):
|
- sqflite (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
@@ -90,17 +96,19 @@ DEPENDENCIES:
|
|||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||||
|
- open_filex (from `.symlinks/plugins/open_filex/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
|
||||||
@@ -128,6 +136,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||||
|
flutter_local_notifications:
|
||||||
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
@@ -136,10 +146,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
local_auth_ios:
|
local_auth_ios:
|
||||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||||
|
open_filex:
|
||||||
|
:path: ".symlinks/plugins/open_filex/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
pdfx:
|
pdfx:
|
||||||
:path: ".symlinks/plugins/pdfx/ios"
|
:path: ".symlinks/plugins/pdfx/ios"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
@@ -148,8 +160,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_ios:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
:path: ".symlinks/plugins/sqflite/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@@ -160,28 +172,30 @@ SPEC CHECKSUMS:
|
|||||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
edge_detection: 9bc5ee35073b5a17c0b3b679908f01017ce3062a
|
edge_detection: fa02aa120e00d87ada0ca2430b6c6087a501b1e9
|
||||||
file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1
|
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
|
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
|
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5
|
||||||
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
local_auth_ios: 0d333dde7780f669e66f19d2ff6005f3ea84008d
|
||||||
|
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||||
pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec
|
pdfx: 7b876b09de8b7a0bf444a4f82b439ffcff4ee1ec
|
||||||
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
|
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
|
||||||
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
|
SDWebImage: 23d714cd599354ee7906dbae26dff89b421c4370
|
||||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||||
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
WeScan: fed582f6c38014d529afb5aa9ffd1bad38fc72b7
|
||||||
|
|
||||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 51;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -321,10 +321,12 @@
|
|||||||
};
|
};
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
);
|
);
|
||||||
name = "Thin Binary";
|
name = "Thin Binary";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
@@ -335,6 +337,7 @@
|
|||||||
};
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,5 +65,7 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
7
lib/constants.dart
Normal file
7
lib/constants.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
// Globally accessible variables which are definitely initialized after main().
|
||||||
|
late final PackageInfo packageInfo;
|
||||||
|
late final AndroidDeviceInfo? androidInfo;
|
||||||
|
late final IosDeviceInfo? iosInfo;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
|
||||||
|
|
||||||
class PaperlessServerInformationCubit
|
class PaperlessServerInformationCubit
|
||||||
extends Cubit<PaperlessServerInformationState> {
|
extends Cubit<PaperlessServerInformationState> {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class PaperlessStatisticsState {
|
|
||||||
final bool isLoaded;
|
|
||||||
final PaperlessServerStatisticsModel? statistics;
|
|
||||||
|
|
||||||
PaperlessStatisticsState({
|
|
||||||
required this.isLoaded,
|
|
||||||
this.statistics,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
54
lib/core/notifier/document_changed_notifier.dart
Normal file
54
lib/core/notifier/document_changed_notifier.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:rxdart/subjects.dart';
|
||||||
|
|
||||||
|
typedef DocumentChangedCallback = void Function(DocumentModel document);
|
||||||
|
|
||||||
|
class DocumentChangedNotifier {
|
||||||
|
final Subject<DocumentModel> _updated = PublishSubject();
|
||||||
|
final Subject<DocumentModel> _deleted = PublishSubject();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribe(
|
||||||
|
dynamic subscriber, {
|
||||||
|
DocumentChangedCallback? onUpdated,
|
||||||
|
DocumentChangedCallback? onDeleted,
|
||||||
|
}) {
|
||||||
|
_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() {
|
||||||
|
_updated.close();
|
||||||
|
_deleted.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:rxdart/subjects.dart';
|
import 'package:rxdart/subjects.dart';
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Base repository class which all repositories should implement
|
/// Base repository class which all repositories should implement
|
||||||
///
|
///
|
||||||
abstract class BaseRepository<State extends RepositoryState, Type>
|
abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
|
||||||
extends Cubit<State> with HydratedMixin {
|
with HydratedMixin {
|
||||||
final State _initialState;
|
final IndexedRepositoryState<T> _initialState;
|
||||||
|
|
||||||
BaseRepository(this._initialState) : super(_initialState) {
|
BaseRepository(this._initialState) : super(_initialState) {
|
||||||
hydrate();
|
hydrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<State?> get values =>
|
Stream<IndexedRepositoryState<T>?> get values =>
|
||||||
BehaviorSubject.seeded(state)..addStream(super.stream);
|
BehaviorSubject.seeded(state)..addStream(super.stream);
|
||||||
|
|
||||||
State? get current => state;
|
IndexedRepositoryState<T>? get current => state;
|
||||||
|
|
||||||
bool get isInitialized => state.hasLoaded;
|
bool get isInitialized => state.hasLoaded;
|
||||||
|
|
||||||
Future<Type> create(Type object);
|
Future<T> create(T object);
|
||||||
Future<Type?> find(int id);
|
Future<T?> find(int id);
|
||||||
Future<Iterable<Type>> findAll([Iterable<int>? ids]);
|
Future<Iterable<T>> findAll([Iterable<int>? ids]);
|
||||||
Future<Type> update(Type object);
|
Future<T> update(T object);
|
||||||
Future<int> delete(Type object);
|
Future<int> delete(T object);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
|
||||||
|
|
||||||
class CorrespondentRepositoryImpl
|
class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
|
||||||
extends LabelRepository<Correspondent, CorrespondentRepositoryState> {
|
|
||||||
final PaperlessLabelsApi _api;
|
final PaperlessLabelsApi _api;
|
||||||
|
|
||||||
CorrespondentRepositoryImpl(this._api)
|
CorrespondentRepositoryImpl(this._api)
|
||||||
@@ -15,7 +13,7 @@ class CorrespondentRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<Correspondent> create(Correspondent correspondent) async {
|
Future<Correspondent> create(Correspondent correspondent) async {
|
||||||
final created = await _api.saveCorrespondent(correspondent);
|
final created = await _api.saveCorrespondent(correspondent);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..putIfAbsent(created.id!, () => created);
|
..putIfAbsent(created.id!, () => created);
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return created;
|
return created;
|
||||||
@@ -24,7 +22,7 @@ class CorrespondentRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<int> delete(Correspondent correspondent) async {
|
Future<int> delete(Correspondent correspondent) async {
|
||||||
await _api.deleteCorrespondent(correspondent);
|
await _api.deleteCorrespondent(correspondent);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..removeWhere((k, v) => k == correspondent.id);
|
..removeWhere((k, v) => k == correspondent.id);
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return correspondent.id!;
|
return correspondent.id!;
|
||||||
@@ -34,7 +32,7 @@ class CorrespondentRepositoryImpl
|
|||||||
Future<Correspondent?> find(int id) async {
|
Future<Correspondent?> find(int id) async {
|
||||||
final correspondent = await _api.getCorrespondent(id);
|
final correspondent = await _api.getCorrespondent(id);
|
||||||
if (correspondent != null) {
|
if (correspondent != null) {
|
||||||
final updatedState = {...state.values}..[id] = correspondent;
|
final updatedState = {...state.values ?? {}}..[id] = correspondent;
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return correspondent;
|
return correspondent;
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,7 @@ class CorrespondentRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
|
||||||
final correspondents = await _api.getCorrespondents(ids);
|
final correspondents = await _api.getCorrespondents(ids);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return correspondents;
|
return correspondents;
|
||||||
@@ -53,7 +51,8 @@ class CorrespondentRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<Correspondent> update(Correspondent correspondent) async {
|
Future<Correspondent> update(Correspondent correspondent) async {
|
||||||
final updated = await _api.updateCorrespondent(correspondent);
|
final updated = await _api.updateCorrespondent(correspondent);
|
||||||
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
|
final updatedState = {...state.values ?? {}}
|
||||||
|
..update(updated.id!, (_) => updated);
|
||||||
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -64,7 +63,7 @@ class CorrespondentRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(CorrespondentRepositoryState state) {
|
Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
|
||||||
|
|
||||||
class DocumentTypeRepositoryImpl
|
class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
|
||||||
extends LabelRepository<DocumentType, DocumentTypeRepositoryState> {
|
|
||||||
final PaperlessLabelsApi _api;
|
final PaperlessLabelsApi _api;
|
||||||
|
|
||||||
DocumentTypeRepositoryImpl(this._api)
|
DocumentTypeRepositoryImpl(this._api)
|
||||||
@@ -13,7 +11,7 @@ class DocumentTypeRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<DocumentType> create(DocumentType documentType) async {
|
Future<DocumentType> create(DocumentType documentType) async {
|
||||||
final created = await _api.saveDocumentType(documentType);
|
final created = await _api.saveDocumentType(documentType);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..putIfAbsent(created.id!, () => created);
|
..putIfAbsent(created.id!, () => created);
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return created;
|
return created;
|
||||||
@@ -22,7 +20,7 @@ class DocumentTypeRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<int> delete(DocumentType documentType) async {
|
Future<int> delete(DocumentType documentType) async {
|
||||||
await _api.deleteDocumentType(documentType);
|
await _api.deleteDocumentType(documentType);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..removeWhere((k, v) => k == documentType.id);
|
..removeWhere((k, v) => k == documentType.id);
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return documentType.id!;
|
return documentType.id!;
|
||||||
@@ -32,7 +30,7 @@ class DocumentTypeRepositoryImpl
|
|||||||
Future<DocumentType?> find(int id) async {
|
Future<DocumentType?> find(int id) async {
|
||||||
final documentType = await _api.getDocumentType(id);
|
final documentType = await _api.getDocumentType(id);
|
||||||
if (documentType != null) {
|
if (documentType != null) {
|
||||||
final updatedState = {...state.values}..[id] = documentType;
|
final updatedState = {...state.values ?? {}}..[id] = documentType;
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return documentType;
|
return documentType;
|
||||||
}
|
}
|
||||||
@@ -42,7 +40,7 @@ class DocumentTypeRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
|
||||||
final documentTypes = await _api.getDocumentTypes(ids);
|
final documentTypes = await _api.getDocumentTypes(ids);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return documentTypes;
|
return documentTypes;
|
||||||
@@ -51,7 +49,8 @@ class DocumentTypeRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<DocumentType> update(DocumentType documentType) async {
|
Future<DocumentType> update(DocumentType documentType) async {
|
||||||
final updated = await _api.updateDocumentType(documentType);
|
final updated = await _api.updateDocumentType(documentType);
|
||||||
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
|
final updatedState = {...state.values ?? {}}
|
||||||
|
..update(updated.id!, (_) => updated);
|
||||||
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -62,7 +61,7 @@ class DocumentTypeRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(DocumentTypeRepositoryState state) {
|
Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
|||||||
@override
|
@override
|
||||||
Future<SavedView> create(SavedView object) async {
|
Future<SavedView> create(SavedView object) async {
|
||||||
final created = await _api.save(object);
|
final created = await _api.save(object);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..putIfAbsent(created.id!, () => created);
|
..putIfAbsent(created.id!, () => created);
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return created;
|
return created;
|
||||||
@@ -19,7 +19,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
|||||||
@override
|
@override
|
||||||
Future<int> delete(SavedView view) async {
|
Future<int> delete(SavedView view) async {
|
||||||
await _api.delete(view);
|
await _api.delete(view);
|
||||||
final updatedState = {...state.values}..remove(view.id);
|
final updatedState = {...state.values ?? {}}..remove(view.id);
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return view.id!;
|
return view.id!;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
|||||||
@override
|
@override
|
||||||
Future<SavedView?> find(int id) async {
|
Future<SavedView?> find(int id) async {
|
||||||
final found = await _api.find(id);
|
final found = await _api.find(id);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..update(id, (_) => found, ifAbsent: () => found);
|
..update(id, (_) => found, ifAbsent: () => found);
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return found;
|
return found;
|
||||||
@@ -37,7 +37,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
|||||||
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
|
||||||
final found = await _api.findAll(ids);
|
final found = await _api.findAll(ids);
|
||||||
final updatedState = {
|
final updatedState = {
|
||||||
...state.values,
|
...state.values ?? {},
|
||||||
...{for (final view in found) view.id!: view},
|
...{for (final view in found) view.id!: view},
|
||||||
};
|
};
|
||||||
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
@@ -56,7 +56,7 @@ class SavedViewRepositoryImpl extends SavedViewRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(SavedViewRepositoryState state) {
|
Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
||||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||||
|
|
||||||
class StoragePathRepositoryImpl
|
class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
|
||||||
extends LabelRepository<StoragePath, StoragePathRepositoryState> {
|
|
||||||
final PaperlessLabelsApi _api;
|
final PaperlessLabelsApi _api;
|
||||||
|
|
||||||
StoragePathRepositoryImpl(this._api)
|
StoragePathRepositoryImpl(this._api)
|
||||||
@@ -13,7 +12,7 @@ class StoragePathRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<StoragePath> create(StoragePath storagePath) async {
|
Future<StoragePath> create(StoragePath storagePath) async {
|
||||||
final created = await _api.saveStoragePath(storagePath);
|
final created = await _api.saveStoragePath(storagePath);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..putIfAbsent(created.id!, () => created);
|
..putIfAbsent(created.id!, () => created);
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return created;
|
return created;
|
||||||
@@ -22,7 +21,7 @@ class StoragePathRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<int> delete(StoragePath storagePath) async {
|
Future<int> delete(StoragePath storagePath) async {
|
||||||
await _api.deleteStoragePath(storagePath);
|
await _api.deleteStoragePath(storagePath);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..removeWhere((k, v) => k == storagePath.id);
|
..removeWhere((k, v) => k == storagePath.id);
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return storagePath.id!;
|
return storagePath.id!;
|
||||||
@@ -32,7 +31,7 @@ class StoragePathRepositoryImpl
|
|||||||
Future<StoragePath?> find(int id) async {
|
Future<StoragePath?> find(int id) async {
|
||||||
final storagePath = await _api.getStoragePath(id);
|
final storagePath = await _api.getStoragePath(id);
|
||||||
if (storagePath != null) {
|
if (storagePath != null) {
|
||||||
final updatedState = {...state.values}..[id] = storagePath;
|
final updatedState = {...state.values ?? {}}..[id] = storagePath;
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return storagePath;
|
return storagePath;
|
||||||
}
|
}
|
||||||
@@ -42,7 +41,7 @@ class StoragePathRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
|
||||||
final storagePaths = await _api.getStoragePaths(ids);
|
final storagePaths = await _api.getStoragePaths(ids);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return storagePaths;
|
return storagePaths;
|
||||||
@@ -51,7 +50,8 @@ class StoragePathRepositoryImpl
|
|||||||
@override
|
@override
|
||||||
Future<StoragePath> update(StoragePath storagePath) async {
|
Future<StoragePath> update(StoragePath storagePath) async {
|
||||||
final updated = await _api.updateStoragePath(storagePath);
|
final updated = await _api.updateStoragePath(storagePath);
|
||||||
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
|
final updatedState = {...state.values ?? {}}
|
||||||
|
..update(updated.id!, (_) => updated);
|
||||||
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ class StoragePathRepositoryImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson(StoragePathRepositoryState state) {
|
Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
|
||||||
|
|
||||||
class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
class TagRepositoryImpl extends LabelRepository<Tag> {
|
||||||
final PaperlessLabelsApi _api;
|
final PaperlessLabelsApi _api;
|
||||||
|
|
||||||
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
|
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
|
||||||
@@ -12,7 +10,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
@override
|
@override
|
||||||
Future<Tag> create(Tag object) async {
|
Future<Tag> create(Tag object) async {
|
||||||
final created = await _api.saveTag(object);
|
final created = await _api.saveTag(object);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..putIfAbsent(created.id!, () => created);
|
..putIfAbsent(created.id!, () => created);
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return created;
|
return created;
|
||||||
@@ -21,7 +19,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
@override
|
@override
|
||||||
Future<int> delete(Tag tag) async {
|
Future<int> delete(Tag tag) async {
|
||||||
await _api.deleteTag(tag);
|
await _api.deleteTag(tag);
|
||||||
final updatedState = {...state.values}..removeWhere((k, v) => k == tag.id);
|
final updatedState = {...state.values ?? {}}
|
||||||
|
..removeWhere((k, v) => k == tag.id);
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return tag.id!;
|
return tag.id!;
|
||||||
}
|
}
|
||||||
@@ -30,7 +29,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
Future<Tag?> find(int id) async {
|
Future<Tag?> find(int id) async {
|
||||||
final tag = await _api.getTag(id);
|
final tag = await _api.getTag(id);
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
final updatedState = {...state.values}..[id] = tag;
|
final updatedState = {...state.values ?? {}}..[id] = tag;
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
@@ -40,7 +39,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
@override
|
@override
|
||||||
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
|
||||||
final tags = await _api.getTags(ids);
|
final tags = await _api.getTags(ids);
|
||||||
final updatedState = {...state.values}
|
final updatedState = {...state.values ?? {}}
|
||||||
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return tags;
|
return tags;
|
||||||
@@ -49,7 +48,8 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
@override
|
@override
|
||||||
Future<Tag> update(Tag tag) async {
|
Future<Tag> update(Tag tag) async {
|
||||||
final updated = await _api.updateTag(tag);
|
final updated = await _api.updateTag(tag);
|
||||||
final updatedState = {...state.values}..update(updated.id!, (_) => updated);
|
final updatedState = {...state.values ?? {}}
|
||||||
|
..update(updated.id!, (_) => updated);
|
||||||
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ class TagRepositoryImpl extends LabelRepository<Tag, TagRepositoryState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic>? toJson(TagRepositoryState state) {
|
Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
|
|
||||||
abstract class LabelRepository<T extends Label, State extends RepositoryState>
|
abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
|
||||||
extends BaseRepository<State, T> {
|
LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
|
||||||
LabelRepository(State initial) : super(initial);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,20 +17,16 @@ class LabelRepositoriesProvider extends StatelessWidget {
|
|||||||
return MultiRepositoryProvider(
|
return MultiRepositoryProvider(
|
||||||
providers: [
|
providers: [
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) => context
|
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) =>
|
create: (context) => context.read<LabelRepository<Tag>>(),
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: child,
|
child: child,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
import 'package:paperless_mobile/core/repository/base_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
|
|
||||||
abstract class SavedViewRepository
|
abstract class SavedViewRepository extends BaseRepository<SavedView> {
|
||||||
extends BaseRepository<SavedViewRepositoryState, SavedView> {
|
|
||||||
SavedViewRepository(super.initialState);
|
SavedViewRepository(super.initialState);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
|
|
||||||
part 'correspondent_repository_state.g.dart';
|
part 'correspondent_repository_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class CorrespondentRepositoryState
|
class CorrespondentRepositoryState
|
||||||
extends RepositoryState<Map<int, Correspondent>> {
|
extends IndexedRepositoryState<Correspondent> {
|
||||||
const CorrespondentRepositoryState({
|
const CorrespondentRepositoryState({
|
||||||
super.values = const {},
|
super.values = const {},
|
||||||
super.hasLoaded,
|
super.hasLoaded,
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ CorrespondentRepositoryState _$CorrespondentRepositoryStateFromJson(
|
|||||||
Map<String, dynamic> _$CorrespondentRepositoryStateToJson(
|
Map<String, dynamic> _$CorrespondentRepositoryStateToJson(
|
||||||
CorrespondentRepositoryState instance) =>
|
CorrespondentRepositoryState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
|
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
|
||||||
'hasLoaded': instance.hasLoaded,
|
'hasLoaded': instance.hasLoaded,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'document_type_repository_state.g.dart';
|
part 'document_type_repository_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class DocumentTypeRepositoryState
|
class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
|
||||||
extends RepositoryState<Map<int, DocumentType>> {
|
|
||||||
const DocumentTypeRepositoryState({
|
const DocumentTypeRepositoryState({
|
||||||
super.values = const {},
|
super.values = const {},
|
||||||
super.hasLoaded,
|
super.hasLoaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DocumentTypeRepositoryState copyWith(
|
DocumentTypeRepositoryState copyWith({
|
||||||
{Map<int, DocumentType>? values, bool? hasLoaded}) {
|
Map<int, DocumentType>? values,
|
||||||
|
bool? hasLoaded,
|
||||||
|
}) {
|
||||||
return DocumentTypeRepositoryState(
|
return DocumentTypeRepositoryState(
|
||||||
values: values ?? this.values,
|
values: values ?? this.values,
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ DocumentTypeRepositoryState _$DocumentTypeRepositoryStateFromJson(
|
|||||||
Map<String, dynamic> _$DocumentTypeRepositoryStateToJson(
|
Map<String, dynamic> _$DocumentTypeRepositoryStateToJson(
|
||||||
DocumentTypeRepositoryState instance) =>
|
DocumentTypeRepositoryState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
|
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
|
||||||
'hasLoaded': instance.hasLoaded,
|
'hasLoaded': instance.hasLoaded,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'saved_view_repository_state.g.dart';
|
part 'saved_view_repository_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class SavedViewRepositoryState extends RepositoryState<Map<int, SavedView>> {
|
class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
|
||||||
const SavedViewRepositoryState({
|
const SavedViewRepositoryState({
|
||||||
super.values = const {},
|
super.values = const {},
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
|
|||||||
Map<String, dynamic> _$SavedViewRepositoryStateToJson(
|
Map<String, dynamic> _$SavedViewRepositoryStateToJson(
|
||||||
SavedViewRepositoryState instance) =>
|
SavedViewRepositoryState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
|
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
|
||||||
'hasLoaded': instance.hasLoaded,
|
'hasLoaded': instance.hasLoaded,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
part 'storage_path_repository_state.g.dart';
|
part 'storage_path_repository_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class StoragePathRepositoryState
|
class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
|
||||||
extends RepositoryState<Map<int, StoragePath>> {
|
|
||||||
const StoragePathRepositoryState({
|
const StoragePathRepositoryState({
|
||||||
super.values = const {},
|
super.values = const {},
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
StoragePathRepositoryState copyWith(
|
StoragePathRepositoryState copyWith({
|
||||||
{Map<int, StoragePath>? values, bool? hasLoaded}) {
|
Map<int, StoragePath>? values,
|
||||||
|
bool? hasLoaded,
|
||||||
|
}) {
|
||||||
return StoragePathRepositoryState(
|
return StoragePathRepositoryState(
|
||||||
values: values ?? this.values,
|
values: values ?? this.values,
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ StoragePathRepositoryState _$StoragePathRepositoryStateFromJson(
|
|||||||
Map<String, dynamic> _$StoragePathRepositoryStateToJson(
|
Map<String, dynamic> _$StoragePathRepositoryStateToJson(
|
||||||
StoragePathRepositoryState instance) =>
|
StoragePathRepositoryState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
|
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
|
||||||
'hasLoaded': instance.hasLoaded,
|
'hasLoaded': instance.hasLoaded,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
|
|
||||||
part 'tag_repository_state.g.dart';
|
part 'tag_repository_state.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class TagRepositoryState extends RepositoryState<Map<int, Tag>> {
|
class TagRepositoryState extends IndexedRepositoryState<Tag> {
|
||||||
const TagRepositoryState({
|
const TagRepositoryState({
|
||||||
super.values = const {},
|
super.values = const {},
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TagRepositoryState copyWith({Map<int, Tag>? values, bool? hasLoaded}) {
|
TagRepositoryState copyWith({
|
||||||
|
Map<int, Tag>? values,
|
||||||
|
bool? hasLoaded,
|
||||||
|
}) {
|
||||||
return TagRepositoryState(
|
return TagRepositoryState(
|
||||||
values: values ?? this.values,
|
values: values ?? this.values,
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ TagRepositoryState _$TagRepositoryStateFromJson(Map<String, dynamic> json) =>
|
|||||||
|
|
||||||
Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) =>
|
Map<String, dynamic> _$TagRepositoryStateToJson(TagRepositoryState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'values': instance.values.map((k, e) => MapEntry(k.toString(), e)),
|
'values': instance.values?.map((k, e) => MapEntry(k.toString(), e)),
|
||||||
'hasLoaded': instance.hasLoaded,
|
'hasLoaded': instance.hasLoaded,
|
||||||
};
|
};
|
||||||
|
|||||||
16
lib/core/repository/state/indexed_repository_state.dart
Normal file
16
lib/core/repository/state/indexed_repository_state.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
abstract class IndexedRepositoryState<T> {
|
||||||
|
final Map<int, T>? values;
|
||||||
|
final bool hasLoaded;
|
||||||
|
|
||||||
|
const IndexedRepositoryState({
|
||||||
|
required this.values,
|
||||||
|
this.hasLoaded = false,
|
||||||
|
}) : assert(!(values == null) || !hasLoaded);
|
||||||
|
|
||||||
|
IndexedRepositoryState.loaded(this.values) : hasLoaded = true;
|
||||||
|
|
||||||
|
IndexedRepositoryState<T> copyWith({
|
||||||
|
Map<int, T>? values,
|
||||||
|
bool? hasLoaded,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
abstract class RepositoryState<T> {
|
|
||||||
final T values;
|
|
||||||
final bool hasLoaded;
|
|
||||||
|
|
||||||
const RepositoryState({
|
|
||||||
required this.values,
|
|
||||||
this.hasLoaded = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
RepositoryState.loaded(this.values) : hasLoaded = true;
|
|
||||||
|
|
||||||
RepositoryState<T> copyWith({
|
|
||||||
T? values,
|
|
||||||
bool? hasLoaded,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ class GithubIssueService {
|
|||||||
..tryPutIfAbsent('assignees', () => assignees?.join(','))
|
..tryPutIfAbsent('assignees', () => assignees?.join(','))
|
||||||
..tryPutIfAbsent('project', () => project),
|
..tryPutIfAbsent('project', () => project),
|
||||||
);
|
);
|
||||||
log("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
|
debugPrint("[GitHubIssueService] Creating GitHub issue: " + uri.toString());
|
||||||
launchUrl(
|
launchUrl(
|
||||||
uri,
|
uri,
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:web_socket_channel/io.dart';
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
abstract class StatusService {
|
abstract class StatusService {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:paperless_mobile/core/type/types.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
|
||||||
|
|
||||||
abstract class LocalVault {
|
|
||||||
Future<void> storeAuthenticationInformation(AuthenticationInformation auth);
|
|
||||||
Future<AuthenticationInformation?> loadAuthenticationInformation();
|
|
||||||
Future<ClientCertificate?> loadCertificate();
|
|
||||||
Future<bool> storeApplicationSettings(ApplicationSettingsState settings);
|
|
||||||
Future<ApplicationSettingsState?> loadApplicationSettings();
|
|
||||||
Future<void> clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocalVaultImpl implements LocalVault {
|
|
||||||
static const applicationSettingsKey = "applicationSettings";
|
|
||||||
static const authenticationKey = "authentication";
|
|
||||||
|
|
||||||
final EncryptedSharedPreferences sharedPreferences;
|
|
||||||
|
|
||||||
LocalVaultImpl(this.sharedPreferences);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> storeAuthenticationInformation(
|
|
||||||
AuthenticationInformation auth,
|
|
||||||
) async {
|
|
||||||
await sharedPreferences.setString(
|
|
||||||
authenticationKey,
|
|
||||||
jsonEncode(auth.toJson()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<AuthenticationInformation?> loadAuthenticationInformation() async {
|
|
||||||
if ((await sharedPreferences.getString(authenticationKey)).isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return AuthenticationInformation.fromJson(
|
|
||||||
jsonDecode(await sharedPreferences.getString(authenticationKey)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ClientCertificate?> loadCertificate() async {
|
|
||||||
return loadAuthenticationInformation()
|
|
||||||
.then((value) => value?.clientCertificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
|
|
||||||
return sharedPreferences.setString(
|
|
||||||
applicationSettingsKey,
|
|
||||||
jsonEncode(settings.toJson()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ApplicationSettingsState?> loadApplicationSettings() async {
|
|
||||||
final settings = await sharedPreferences.getString(applicationSettingsKey);
|
|
||||||
if (settings.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return compute(
|
|
||||||
ApplicationSettingsState.fromJson,
|
|
||||||
jsonDecode(settings) as JSON,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> clear() {
|
|
||||||
return sharedPreferences.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
|
String translateColorSchemeOption(
|
||||||
|
BuildContext context, ColorSchemeOption option) {
|
||||||
|
switch (option) {
|
||||||
|
case ColorSchemeOption.classic:
|
||||||
|
return S.of(context).colorSchemeOptionClassic;
|
||||||
|
case ColorSchemeOption.dynamic:
|
||||||
|
return S.of(context).colorSchemeOptionDynamic;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/core/translation/sort_field_localization_mapper.dart
Normal file
24
lib/core/translation/sort_field_localization_mapper.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
|
String translateSortField(BuildContext context, SortField? sortField) {
|
||||||
|
switch (sortField) {
|
||||||
|
case SortField.archiveSerialNumber:
|
||||||
|
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
||||||
|
case SortField.correspondentName:
|
||||||
|
return S.of(context).documentCorrespondentPropertyLabel;
|
||||||
|
case SortField.title:
|
||||||
|
return S.of(context).documentTitlePropertyLabel;
|
||||||
|
case SortField.documentType:
|
||||||
|
return S.of(context).documentDocumentTypePropertyLabel;
|
||||||
|
case SortField.created:
|
||||||
|
return S.of(context).documentCreatedPropertyLabel;
|
||||||
|
case SortField.added:
|
||||||
|
return S.of(context).documentAddedPropertyLabel;
|
||||||
|
case SortField.modified:
|
||||||
|
return S.of(context).documentModifiedPropertyLabel;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:rxdart/subjects.dart';
|
||||||
|
|
||||||
typedef JSON = Map<String, dynamic>;
|
typedef JSON = Map<String, dynamic>;
|
||||||
typedef PaperlessValidationErrors = Map<String, String>;
|
typedef PaperlessValidationErrors = Map<String, String>;
|
||||||
typedef PaperlessLocalizedErrorMessage = String;
|
typedef PaperlessLocalizedErrorMessage = String;
|
||||||
|
|||||||
217
lib/core/widgets/app_options_popup_menu.dart
Normal file
217
lib/core/widgets/app_options_popup_menu.dart
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
// import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
// import 'package:paperless_mobile/constants.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
|
// import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
// import 'package:url_launcher/link.dart';
|
||||||
|
// import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
// /// Declares selectable actions in menu.
|
||||||
|
// enum AppPopupMenuEntries {
|
||||||
|
// // Documents preview
|
||||||
|
// documentsSelectListView,
|
||||||
|
// documentsSelectGridView,
|
||||||
|
// // Generic actions
|
||||||
|
// openAboutThisAppDialog,
|
||||||
|
// reportBug,
|
||||||
|
// openSettings,
|
||||||
|
// // Adds a divider
|
||||||
|
// divider;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// class AppOptionsPopupMenu extends StatelessWidget {
|
||||||
|
// final List<AppPopupMenuEntries> displayedActions;
|
||||||
|
// const AppOptionsPopupMenu({
|
||||||
|
// super.key,
|
||||||
|
// required this.displayedActions,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// return PopupMenuButton<AppPopupMenuEntries>(
|
||||||
|
// position: PopupMenuPosition.under,
|
||||||
|
// icon: const Icon(Icons.more_vert),
|
||||||
|
// onSelected: (action) {
|
||||||
|
// switch (action) {
|
||||||
|
// case AppPopupMenuEntries.documentsSelectListView:
|
||||||
|
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.list);
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.documentsSelectGridView:
|
||||||
|
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.grid);
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
||||||
|
// _showAboutDialog(context);
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.openSettings:
|
||||||
|
// Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => BlocProvider.value(
|
||||||
|
// value: context.read<ApplicationSettingsCubit>(),
|
||||||
|
// child: const SettingsPage(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.reportBug:
|
||||||
|
// launchUrlString(
|
||||||
|
// 'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||||
|
// );
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// itemBuilder: _buildEntries,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PopupMenuItem<AppPopupMenuEntries> _buildReportBugTile(BuildContext context) {
|
||||||
|
// return PopupMenuItem(
|
||||||
|
// value: AppPopupMenuEntries.reportBug,
|
||||||
|
// padding: EdgeInsets.zero,
|
||||||
|
// child: ListTile(
|
||||||
|
// leading: const Icon(Icons.bug_report),
|
||||||
|
// title: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PopupMenuItem<AppPopupMenuEntries> _buildSettingsTile(BuildContext context) {
|
||||||
|
// return PopupMenuItem(
|
||||||
|
// padding: EdgeInsets.zero,
|
||||||
|
// value: AppPopupMenuEntries.openSettings,
|
||||||
|
// child: ListTile(
|
||||||
|
// leading: const Icon(Icons.settings_outlined),
|
||||||
|
// title: Text(S.of(context).appDrawerSettingsLabel),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PopupMenuItem<AppPopupMenuEntries> _buildAboutTile(BuildContext context) {
|
||||||
|
// return PopupMenuItem(
|
||||||
|
// padding: EdgeInsets.zero,
|
||||||
|
// value: AppPopupMenuEntries.openAboutThisAppDialog,
|
||||||
|
// child: ListTile(
|
||||||
|
// leading: const Icon(Icons.info_outline),
|
||||||
|
// title: Text(S.of(context).appDrawerAboutLabel),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PopupMenuItem<AppPopupMenuEntries> _buildListViewTile() {
|
||||||
|
// return PopupMenuItem(
|
||||||
|
// padding: EdgeInsets.zero,
|
||||||
|
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
|
// builder: (context, state) {
|
||||||
|
// return ListTile(
|
||||||
|
// leading: const Icon(Icons.list),
|
||||||
|
// title: const Text("List"),
|
||||||
|
// trailing: state.preferredViewType == ViewType.list
|
||||||
|
// ? const Icon(Icons.check)
|
||||||
|
// : null,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// value: AppPopupMenuEntries.documentsSelectListView,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// PopupMenuItem<AppPopupMenuEntries> _buildGridViewTile() {
|
||||||
|
// return PopupMenuItem(
|
||||||
|
// value: AppPopupMenuEntries.documentsSelectGridView,
|
||||||
|
// padding: EdgeInsets.zero,
|
||||||
|
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
|
// builder: (context, state) {
|
||||||
|
// return ListTile(
|
||||||
|
// leading: const Icon(Icons.grid_view_rounded),
|
||||||
|
// title: const Text("Grid"),
|
||||||
|
// trailing: state.preferredViewType == ViewType.grid
|
||||||
|
// ? const Icon(Icons.check)
|
||||||
|
// : null,
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// void _showAboutDialog(BuildContext context) {
|
||||||
|
// showAboutDialog(
|
||||||
|
// context: context,
|
||||||
|
// applicationIcon: const ImageIcon(
|
||||||
|
// AssetImage('assets/logos/paperless_logo_green.png'),
|
||||||
|
// ),
|
||||||
|
// applicationName: 'Paperless Mobile',
|
||||||
|
// applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
|
||||||
|
// children: [
|
||||||
|
// Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')),
|
||||||
|
// Link(
|
||||||
|
// uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'),
|
||||||
|
// builder: (context, followLink) => GestureDetector(
|
||||||
|
// onTap: followLink,
|
||||||
|
// child: Text(
|
||||||
|
// 'https://github.com/astubenbord/paperless-mobile',
|
||||||
|
// style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// Text(
|
||||||
|
// 'Credits',
|
||||||
|
// style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
// ),
|
||||||
|
// _buildOnboardingImageCredits(),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Widget _buildOnboardingImageCredits() {
|
||||||
|
// return Link(
|
||||||
|
// uri: Uri.parse(
|
||||||
|
// 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'),
|
||||||
|
// builder: (context, followLink) => Wrap(
|
||||||
|
// children: [
|
||||||
|
// const Text('Onboarding images by '),
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: followLink,
|
||||||
|
// child: Text(
|
||||||
|
// 'pch.vector',
|
||||||
|
// style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const Text(' on Freepik.')
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// List<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries(
|
||||||
|
// BuildContext context) {
|
||||||
|
// List<PopupMenuEntry<AppPopupMenuEntries>> items = [];
|
||||||
|
// for (final entry in displayedActions) {
|
||||||
|
// switch (entry) {
|
||||||
|
// case AppPopupMenuEntries.documentsSelectListView:
|
||||||
|
// items.add(_buildListViewTile());
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.documentsSelectGridView:
|
||||||
|
// items.add(_buildGridViewTile());
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
||||||
|
// items.add(_buildAboutTile(context));
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.reportBug:
|
||||||
|
// items.add(_buildReportBugTile(context));
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.openSettings:
|
||||||
|
// items.add(_buildSettingsTile(context));
|
||||||
|
// break;
|
||||||
|
// case AppPopupMenuEntries.divider:
|
||||||
|
// items.add(const PopupMenuDivider());
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return items;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:shimmer/shimmer.dart';
|
|
||||||
|
|
||||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
|
||||||
final List<Widget> above;
|
|
||||||
final List<Widget> below;
|
|
||||||
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;
|
|
||||||
|
|
||||||
const DocumentsListLoadingWidget({
|
|
||||||
super.key,
|
|
||||||
this.above = const [],
|
|
||||||
this.below = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListView(
|
|
||||||
children: <Widget>[
|
|
||||||
...above,
|
|
||||||
...List.generate(25, (idx) {
|
|
||||||
final r = Random(idx);
|
|
||||||
final tagCount = r.nextInt(tags.length + 1);
|
|
||||||
final correspondentLength =
|
|
||||||
correspondentLengths[r.nextInt(correspondentLengths.length - 1)];
|
|
||||||
final titleLength = titleLengths[r.nextInt(titleLengths.length - 1)];
|
|
||||||
return Shimmer.fromColors(
|
|
||||||
baseColor: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.grey[300]!
|
|
||||||
: Colors.grey[900]!,
|
|
||||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
|
||||||
? Colors.grey[100]!
|
|
||||||
: Colors.grey[600]!,
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(8),
|
|
||||||
dense: true,
|
|
||||||
isThreeLine: true,
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.white,
|
|
||||||
height: 50,
|
|
||||||
width: 35,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
|
||||||
width: correspondentLength,
|
|
||||||
height: fontSize,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
|
||||||
height: fontSize,
|
|
||||||
width: titleLength,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
Wrap(
|
|
||||||
spacing: 2.0,
|
|
||||||
children: List.generate(
|
|
||||||
tagCount,
|
|
||||||
(index) => InputChip(
|
|
||||||
label: Text(tags[r.nextInt(tags.length)]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).paddedOnly(top: 4),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
...below,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
class HintCard extends StatelessWidget {
|
class HintCard extends StatelessWidget {
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final double elevation;
|
final double elevation;
|
||||||
|
final IconData hintIcon;
|
||||||
final VoidCallback? onHintAcknowledged;
|
final VoidCallback? onHintAcknowledged;
|
||||||
final bool show;
|
final bool show;
|
||||||
const HintCard({
|
const HintCard({
|
||||||
@@ -13,7 +14,8 @@ class HintCard extends StatelessWidget {
|
|||||||
required this.hintText,
|
required this.hintText,
|
||||||
this.onHintAcknowledged,
|
this.onHintAcknowledged,
|
||||||
this.elevation = 1,
|
this.elevation = 1,
|
||||||
required this.show,
|
this.show = true,
|
||||||
|
this.hintIcon = Icons.tips_and_updates_outlined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,16 +33,19 @@ class HintCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.tips_and_updates_outlined,
|
hintIcon,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
).padded(),
|
).padded(),
|
||||||
Align(
|
Padding(
|
||||||
alignment: Alignment.center,
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Align(
|
||||||
hintText,
|
alignment: Alignment.center,
|
||||||
softWrap: true,
|
child: Text(
|
||||||
textAlign: TextAlign.center,
|
hintText,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onHintAcknowledged != null)
|
if (onHintAcknowledged != null)
|
||||||
@@ -52,7 +57,7 @@ class HintCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Padding(padding: EdgeInsets.only(bottom: 24)),
|
const Padding(padding: EdgeInsets.only(bottom: 24)),
|
||||||
],
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
).padded(),
|
).padded(),
|
||||||
|
|||||||
602
lib/core/widgets/material/search/m3_search.dart
Normal file
602
lib/core/widgets/material/search/m3_search.dart
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
//TODO: REMOVE THIS WHEN NATIVE MATERIAL FLUTTER SEARCH IS RELEASED
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// 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.
|
||||||
|
///
|
||||||
|
/// The search page consists of an app bar with a search field and a body which
|
||||||
|
/// can either show suggested search queries or the search results.
|
||||||
|
///
|
||||||
|
/// The appearance of the search page is determined by the provided
|
||||||
|
/// `delegate`. The initial query string is given by `query`, which defaults
|
||||||
|
/// to the empty string. When `query` is set to null, `delegate.query` will
|
||||||
|
/// be used as the initial query.
|
||||||
|
///
|
||||||
|
/// This method returns the selected search result, which can be set in the
|
||||||
|
/// [SearchDelegate.close] call. If the search page is closed with the system
|
||||||
|
/// back button, it returns null.
|
||||||
|
///
|
||||||
|
/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search]
|
||||||
|
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
|
||||||
|
/// for another [showMaterial3Search] call.
|
||||||
|
///
|
||||||
|
/// The `useRootNavigator` argument is used to determine whether to push the
|
||||||
|
/// search page to the [Navigator] furthest from or nearest to the given
|
||||||
|
/// `context`. By default, `useRootNavigator` is `false` and the search page
|
||||||
|
/// route created by this method is pushed to the nearest navigator to the
|
||||||
|
/// given `context`. It can not be `null`.
|
||||||
|
///
|
||||||
|
/// The transition to the search page triggered by this method looks best if the
|
||||||
|
/// screen triggering the transition contains an [AppBar] at the top and the
|
||||||
|
/// transition is called from an [IconButton] that's part of [AppBar.actions].
|
||||||
|
/// The animation provided by [SearchDelegate.transitionAnimation] can be used
|
||||||
|
/// to trigger additional animations in the underlying page while the search
|
||||||
|
/// page fades in or out. This is commonly used to animate an [AnimatedIcon] in
|
||||||
|
/// the [AppBar.leading] position e.g. from the hamburger menu to the back arrow
|
||||||
|
/// used to exit the search page.
|
||||||
|
///
|
||||||
|
/// ## Handling emojis and other complex characters
|
||||||
|
/// {@macro flutter.widgets.EditableText.onChanged}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [SearchDelegate] to define the content of the search page.
|
||||||
|
Future<T?> showMaterial3Search<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required SearchDelegate<T> delegate,
|
||||||
|
String? query = '',
|
||||||
|
bool useRootNavigator = false,
|
||||||
|
}) {
|
||||||
|
delegate.query = query ?? delegate.query;
|
||||||
|
delegate._currentBody = _SearchBody.suggestions;
|
||||||
|
return Navigator.of(context, rootNavigator: useRootNavigator)
|
||||||
|
.push(_SearchPageRoute<T>(
|
||||||
|
delegate: delegate,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delegate for [showMaterial3Search] to define the content of the search page.
|
||||||
|
///
|
||||||
|
/// The search page always shows an [AppBar] at the top where users can
|
||||||
|
/// enter their search queries. The buttons shown before and after the search
|
||||||
|
/// query text field can be customized via [SearchDelegate.buildLeading]
|
||||||
|
/// and [SearchDelegate.buildActions]. Additionally, a widget can be placed
|
||||||
|
/// across the bottom of the [AppBar] via [SearchDelegate.buildBottom].
|
||||||
|
///
|
||||||
|
/// The body below the [AppBar] can either show suggested queries (returned by
|
||||||
|
/// [SearchDelegate.buildSuggestions]) or - once the user submits a search - the
|
||||||
|
/// results of the search as returned by [SearchDelegate.buildResults].
|
||||||
|
///
|
||||||
|
/// [SearchDelegate.query] always contains the current query entered by the user
|
||||||
|
/// and should be used to build the suggestions and results.
|
||||||
|
///
|
||||||
|
/// The results can be brought on screen by calling [SearchDelegate.showResults]
|
||||||
|
/// and you can go back to showing the suggestions by calling
|
||||||
|
/// [SearchDelegate.showSuggestions].
|
||||||
|
///
|
||||||
|
/// Once the user has selected a search result, [SearchDelegate.close] should be
|
||||||
|
/// called to remove the search page from the top of the navigation stack and
|
||||||
|
/// to notify the caller of [showMaterial3Search] about the selected search result.
|
||||||
|
///
|
||||||
|
/// A given [SearchDelegate] can only be associated with one active [showMaterial3Search]
|
||||||
|
/// call. Call [SearchDelegate.close] before re-using the same delegate instance
|
||||||
|
/// for another [showMaterial3Search] call.
|
||||||
|
///
|
||||||
|
/// ## Handling emojis and other complex characters
|
||||||
|
/// {@macro flutter.widgets.EditableText.onChanged}
|
||||||
|
abstract class SearchDelegate<T> {
|
||||||
|
/// Constructor to be called by subclasses which may specify
|
||||||
|
/// [searchFieldLabel], either [searchFieldStyle] or [searchFieldDecorationTheme],
|
||||||
|
/// [keyboardType] and/or [textInputAction]. Only one of [searchFieldLabel]
|
||||||
|
/// and [searchFieldDecorationTheme] may be non-null.
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
/// ```dart
|
||||||
|
/// class CustomSearchHintDelegate extends SearchDelegate<String> {
|
||||||
|
/// CustomSearchHintDelegate({
|
||||||
|
/// required String hintText,
|
||||||
|
/// }) : super(
|
||||||
|
/// searchFieldLabel: hintText,
|
||||||
|
/// keyboardType: TextInputType.text,
|
||||||
|
/// textInputAction: TextInputAction.search,
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// Widget buildLeading(BuildContext context) => const Text('leading');
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// PreferredSizeWidget buildBottom(BuildContext context) {
|
||||||
|
/// return const PreferredSize(
|
||||||
|
/// preferredSize: Size.fromHeight(56.0),
|
||||||
|
/// child: Text('bottom'));
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// Widget buildSuggestions(BuildContext context) => const Text('suggestions');
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// Widget buildResults(BuildContext context) => const Text('results');
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// List<Widget> buildActions(BuildContext context) => <Widget>[];
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
SearchDelegate({
|
||||||
|
this.searchFieldLabel,
|
||||||
|
this.searchFieldStyle,
|
||||||
|
this.searchFieldDecorationTheme,
|
||||||
|
this.keyboardType,
|
||||||
|
this.textInputAction = TextInputAction.search,
|
||||||
|
}) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null);
|
||||||
|
|
||||||
|
/// Suggestions shown in the body of the search page while the user types a
|
||||||
|
/// query into the search field.
|
||||||
|
///
|
||||||
|
/// The delegate method is called whenever the content of [query] changes.
|
||||||
|
/// The suggestions should be based on the current [query] string. If the query
|
||||||
|
/// string is empty, it is good practice to show suggested queries based on
|
||||||
|
/// past queries or the current context.
|
||||||
|
///
|
||||||
|
/// Usually, this method will return a [ListView] with one [ListTile] per
|
||||||
|
/// suggestion. When [ListTile.onTap] is called, [query] should be updated
|
||||||
|
/// with the corresponding suggestion and the results page should be shown
|
||||||
|
/// by calling [showResults].
|
||||||
|
Widget buildSuggestions(BuildContext context);
|
||||||
|
|
||||||
|
/// The results shown after the user submits a search from the search page.
|
||||||
|
///
|
||||||
|
/// The current value of [query] can be used to determine what the user
|
||||||
|
/// searched for.
|
||||||
|
///
|
||||||
|
/// This method might be applied more than once to the same query.
|
||||||
|
/// If your [buildResults] method is computationally expensive, you may want
|
||||||
|
/// to cache the search results for one or more queries.
|
||||||
|
///
|
||||||
|
/// Typically, this method returns a [ListView] with the search results.
|
||||||
|
/// When the user taps on a particular search result, [close] should be called
|
||||||
|
/// with the selected result as argument. This will close the search page and
|
||||||
|
/// communicate the result back to the initial caller of [showMaterial3Search].
|
||||||
|
Widget buildResults(BuildContext context);
|
||||||
|
|
||||||
|
/// A widget to display before the current query in the [AppBar].
|
||||||
|
///
|
||||||
|
/// Typically an [IconButton] configured with a [BackButtonIcon] that exits
|
||||||
|
/// the search with [close]. One can also use an [AnimatedIcon] driven by
|
||||||
|
/// [transitionAnimation], which animates from e.g. a hamburger menu to the
|
||||||
|
/// back button as the search overlay fades in.
|
||||||
|
///
|
||||||
|
/// Returns null if no widget should be shown.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AppBar.leading], the intended use for the return value of this method.
|
||||||
|
Widget? buildLeading(BuildContext context);
|
||||||
|
|
||||||
|
/// Widgets to display after the search query in the [AppBar].
|
||||||
|
///
|
||||||
|
/// If the [query] is not empty, this should typically contain a button to
|
||||||
|
/// clear the query and show the suggestions again (via [showSuggestions]) if
|
||||||
|
/// the results are currently shown.
|
||||||
|
///
|
||||||
|
/// Returns null if no widget should be shown.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AppBar.actions], the intended use for the return value of this method.
|
||||||
|
List<Widget>? buildActions(BuildContext context);
|
||||||
|
|
||||||
|
/// Widget to display across the bottom of the [AppBar].
|
||||||
|
///
|
||||||
|
/// Returns null by default, i.e. a bottom widget is not included.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AppBar.bottom], the intended use for the return value of this method.
|
||||||
|
///
|
||||||
|
PreferredSizeWidget? buildBottom(BuildContext context) => null;
|
||||||
|
|
||||||
|
/// The theme used to configure the search page.
|
||||||
|
///
|
||||||
|
/// The returned [ThemeData] will be used to wrap the entire search page,
|
||||||
|
/// so it can be used to configure any of its components with the appropriate
|
||||||
|
/// theme properties.
|
||||||
|
///
|
||||||
|
/// Unless overridden, the default theme will configure the AppBar containing
|
||||||
|
/// the search input text field with a white background and black text on light
|
||||||
|
/// themes. For dark themes the default is a dark grey background with light
|
||||||
|
/// color text.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AppBarTheme], which configures the AppBar's appearance.
|
||||||
|
/// * [InputDecorationTheme], which configures the appearance of the search
|
||||||
|
/// text field.
|
||||||
|
ThemeData appBarTheme(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final ColorScheme colorScheme = theme.colorScheme;
|
||||||
|
return theme.copyWith(
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: searchFieldDecorationTheme ??
|
||||||
|
InputDecorationTheme(
|
||||||
|
hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current query string shown in the [AppBar].
|
||||||
|
///
|
||||||
|
/// The user manipulates this string via the keyboard.
|
||||||
|
///
|
||||||
|
/// If the user taps on a suggestion provided by [buildSuggestions] this
|
||||||
|
/// string should be updated to that suggestion via the setter.
|
||||||
|
String get query => _queryTextController.text;
|
||||||
|
|
||||||
|
/// Changes the current query string.
|
||||||
|
///
|
||||||
|
/// Setting the query string programmatically moves the cursor to the end of the text field.
|
||||||
|
set query(String value) {
|
||||||
|
assert(query != null);
|
||||||
|
_queryTextController.text = value;
|
||||||
|
if (_queryTextController.text.isNotEmpty) {
|
||||||
|
_queryTextController.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: _queryTextController.text.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition from the suggestions returned by [buildSuggestions] to the
|
||||||
|
/// [query] results returned by [buildResults].
|
||||||
|
///
|
||||||
|
/// If the user taps on a suggestion provided by [buildSuggestions] the
|
||||||
|
/// screen should typically transition to the page showing the search
|
||||||
|
/// results for the suggested query. This transition can be triggered
|
||||||
|
/// by calling this method.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [showSuggestions] to show the search suggestions again.
|
||||||
|
void showResults(BuildContext context) {
|
||||||
|
_focusNode?.unfocus();
|
||||||
|
_currentBody = _SearchBody.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition from showing the results returned by [buildResults] to showing
|
||||||
|
/// the suggestions returned by [buildSuggestions].
|
||||||
|
///
|
||||||
|
/// Calling this method will also put the input focus back into the search
|
||||||
|
/// field of the [AppBar].
|
||||||
|
///
|
||||||
|
/// If the results are currently shown this method can be used to go back
|
||||||
|
/// to showing the search suggestions.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [showResults] to show the search results.
|
||||||
|
void showSuggestions(BuildContext context) {
|
||||||
|
assert(_focusNode != null,
|
||||||
|
'_focusNode must be set by route before showSuggestions is called.');
|
||||||
|
_focusNode!.requestFocus();
|
||||||
|
_currentBody = _SearchBody.suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the search page and returns to the underlying route.
|
||||||
|
///
|
||||||
|
/// The value provided for `result` is used as the return value of the call
|
||||||
|
/// to [showMaterial3Search] that launched the search initially.
|
||||||
|
void close(BuildContext context, T result) {
|
||||||
|
_currentBody = null;
|
||||||
|
_focusNode?.unfocus();
|
||||||
|
Navigator.of(context)
|
||||||
|
..popUntil((Route<dynamic> route) => route == _route)
|
||||||
|
..pop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hint text that is shown in the search field when it is empty.
|
||||||
|
///
|
||||||
|
/// If this value is set to null, the value of
|
||||||
|
/// `MaterialLocalizations.of(context).searchFieldLabel` will be used instead.
|
||||||
|
final String? searchFieldLabel;
|
||||||
|
|
||||||
|
/// The style of the [searchFieldLabel].
|
||||||
|
///
|
||||||
|
/// If this value is set to null, the value of the ambient [Theme]'s
|
||||||
|
/// [InputDecorationTheme.hintStyle] will be used instead.
|
||||||
|
///
|
||||||
|
/// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can
|
||||||
|
/// be non-null.
|
||||||
|
final TextStyle? searchFieldStyle;
|
||||||
|
|
||||||
|
/// The [InputDecorationTheme] used to configure the search field's visuals.
|
||||||
|
///
|
||||||
|
/// Only one of [searchFieldStyle] or [searchFieldDecorationTheme] can
|
||||||
|
/// be non-null.
|
||||||
|
final InputDecorationTheme? searchFieldDecorationTheme;
|
||||||
|
|
||||||
|
/// The type of action button to use for the keyboard.
|
||||||
|
///
|
||||||
|
/// Defaults to the default value specified in [TextField].
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
|
||||||
|
/// The text input action configuring the soft keyboard to a particular action
|
||||||
|
/// button.
|
||||||
|
///
|
||||||
|
/// Defaults to [TextInputAction.search].
|
||||||
|
final TextInputAction textInputAction;
|
||||||
|
|
||||||
|
/// [Animation] triggered when the search pages fades in or out.
|
||||||
|
///
|
||||||
|
/// This animation is commonly used to animate [AnimatedIcon]s of
|
||||||
|
/// [IconButton]s returned by [buildLeading] or [buildActions]. It can also be
|
||||||
|
/// used to animate [IconButton]s contained within the route below the search
|
||||||
|
/// page.
|
||||||
|
Animation<double> get transitionAnimation => _proxyAnimation;
|
||||||
|
|
||||||
|
// The focus node to use for manipulating focus on the search page. This is
|
||||||
|
// managed, owned, and set by the _SearchPageRoute using this delegate.
|
||||||
|
FocusNode? _focusNode;
|
||||||
|
|
||||||
|
final TextEditingController _queryTextController = TextEditingController();
|
||||||
|
|
||||||
|
final ProxyAnimation _proxyAnimation =
|
||||||
|
ProxyAnimation(kAlwaysDismissedAnimation);
|
||||||
|
|
||||||
|
final ValueNotifier<_SearchBody?> _currentBodyNotifier =
|
||||||
|
ValueNotifier<_SearchBody?>(null);
|
||||||
|
|
||||||
|
_SearchBody? get _currentBody => _currentBodyNotifier.value;
|
||||||
|
set _currentBody(_SearchBody? value) {
|
||||||
|
_currentBodyNotifier.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_SearchPageRoute<T>? _route;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the body that is currently shown under the [AppBar] in the
|
||||||
|
/// search page.
|
||||||
|
enum _SearchBody {
|
||||||
|
/// Suggested queries are shown in the body.
|
||||||
|
///
|
||||||
|
/// The suggested queries are generated by [SearchDelegate.buildSuggestions].
|
||||||
|
suggestions,
|
||||||
|
|
||||||
|
/// Search results are currently shown in the body.
|
||||||
|
///
|
||||||
|
/// The search results are generated by [SearchDelegate.buildResults].
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageRoute<T> extends PageRoute<T> {
|
||||||
|
_SearchPageRoute({
|
||||||
|
required this.delegate,
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
delegate._route == null,
|
||||||
|
'The ${delegate.runtimeType} instance is currently used by another active '
|
||||||
|
'search. Please close that search by calling close() on the SearchDelegate '
|
||||||
|
'before opening another search with the same delegate instance.',
|
||||||
|
);
|
||||||
|
delegate._route = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final SearchDelegate<T> delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Animation<double> createAnimation() {
|
||||||
|
final Animation<double> animation = super.createAnimation();
|
||||||
|
delegate._proxyAnimation.parent = animation;
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
return _SearchPage<T>(
|
||||||
|
delegate: delegate,
|
||||||
|
animation: animation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didComplete(T? result) {
|
||||||
|
super.didComplete(result);
|
||||||
|
assert(delegate._route == this);
|
||||||
|
delegate._route = null;
|
||||||
|
delegate._currentBody = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPage<T> extends StatefulWidget {
|
||||||
|
const _SearchPage({
|
||||||
|
required this.delegate,
|
||||||
|
required this.animation,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SearchDelegate<T> delegate;
|
||||||
|
final Animation<double> animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _SearchPageState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchPageState<T> extends State<_SearchPage<T>> {
|
||||||
|
// This node is owned, but not hosted by, the search page. Hosting is done by
|
||||||
|
// the text field.
|
||||||
|
FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.delegate._queryTextController.addListener(_onQueryChanged);
|
||||||
|
widget.animation.addStatusListener(_onAnimationStatusChanged);
|
||||||
|
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||||
|
focusNode.addListener(_onFocusChanged);
|
||||||
|
widget.delegate._focusNode = focusNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
widget.delegate._queryTextController.removeListener(_onQueryChanged);
|
||||||
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||||
|
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
|
||||||
|
widget.delegate._focusNode = null;
|
||||||
|
focusNode.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||||
|
if (status != AnimationStatus.completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||||
|
if (widget.delegate._currentBody == _SearchBody.suggestions) {
|
||||||
|
focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(_SearchPage<T> oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.delegate != oldWidget.delegate) {
|
||||||
|
oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
|
||||||
|
widget.delegate._queryTextController.addListener(_onQueryChanged);
|
||||||
|
oldWidget.delegate._currentBodyNotifier
|
||||||
|
.removeListener(_onSearchBodyChanged);
|
||||||
|
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
|
||||||
|
oldWidget.delegate._focusNode = null;
|
||||||
|
widget.delegate._focusNode = focusNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
if (focusNode.hasFocus &&
|
||||||
|
widget.delegate._currentBody != _SearchBody.suggestions) {
|
||||||
|
widget.delegate.showSuggestions(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onQueryChanged() {
|
||||||
|
setState(() {
|
||||||
|
// rebuild ourselves because query changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearchBodyChanged() {
|
||||||
|
setState(() {
|
||||||
|
// rebuild ourselves because search body changed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterialLocalizations(context));
|
||||||
|
final ThemeData theme = widget.delegate.appBarTheme(context);
|
||||||
|
final String searchFieldLabel = widget.delegate.searchFieldLabel ??
|
||||||
|
MaterialLocalizations.of(context).searchFieldLabel;
|
||||||
|
Widget? body;
|
||||||
|
switch (widget.delegate._currentBody) {
|
||||||
|
case _SearchBody.suggestions:
|
||||||
|
body = KeyedSubtree(
|
||||||
|
key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
|
||||||
|
child: widget.delegate.buildSuggestions(context),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case _SearchBody.results:
|
||||||
|
body = KeyedSubtree(
|
||||||
|
key: const ValueKey<_SearchBody>(_SearchBody.results),
|
||||||
|
child: widget.delegate.buildResults(context),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final String routeName;
|
||||||
|
switch (theme.platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
routeName = '';
|
||||||
|
break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
routeName = searchFieldLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
explicitChildNodes: true,
|
||||||
|
scopesRoute: true,
|
||||||
|
namesRoute: true,
|
||||||
|
label: routeName,
|
||||||
|
child: Theme(
|
||||||
|
data: theme,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
toolbarHeight: 72,
|
||||||
|
leading: widget.delegate.buildLeading(context),
|
||||||
|
title: TextField(
|
||||||
|
controller: widget.delegate._queryTextController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: widget.delegate.searchFieldStyle ??
|
||||||
|
theme.textTheme.titleLarge,
|
||||||
|
textInputAction: widget.delegate.textInputAction,
|
||||||
|
keyboardType: widget.delegate.keyboardType,
|
||||||
|
onSubmitted: (String _) {
|
||||||
|
widget.delegate.showResults(context);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(hintText: searchFieldLabel),
|
||||||
|
),
|
||||||
|
actions: widget.delegate.buildActions(context),
|
||||||
|
bottom: widget.delegate.buildBottom(context),
|
||||||
|
),
|
||||||
|
body: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/core/widgets/material/search/m3_search_bar.dart
Normal file
79
lib/core/widgets/material/search/m3_search_bar.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SearchBar extends StatelessWidget {
|
||||||
|
const SearchBar({
|
||||||
|
Key? key,
|
||||||
|
this.height = 56,
|
||||||
|
required this.leadingIcon,
|
||||||
|
this.trailingIcon,
|
||||||
|
required this.supportingText,
|
||||||
|
required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final double height;
|
||||||
|
double get effectiveHeight {
|
||||||
|
return max(height, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Widget leadingIcon;
|
||||||
|
final Widget? trailingIcon;
|
||||||
|
|
||||||
|
final String supportingText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minWidth: 360, maxWidth: 720),
|
||||||
|
width: double.infinity,
|
||||||
|
height: effectiveHeight,
|
||||||
|
child: Material(
|
||||||
|
elevation: 1,
|
||||||
|
color: colorScheme.surface,
|
||||||
|
shadowColor: colorScheme.shadow,
|
||||||
|
surfaceTintColor: colorScheme.surfaceTint,
|
||||||
|
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
splashFactory: InkRipple.splashFactory,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(children: [
|
||||||
|
leadingIcon,
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: TextField(
|
||||||
|
onTap: onTap,
|
||||||
|
readOnly: true,
|
||||||
|
enabled: false,
|
||||||
|
cursorColor: colorScheme.primary,
|
||||||
|
style: textTheme.bodyLarge,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isCollapsed: true,
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
hintText: supportingText,
|
||||||
|
hintStyle: textTheme.bodyLarge?.apply(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (trailingIcon != null) trailingIcon!,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,25 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
class PaperlessLogo extends StatelessWidget {
|
class PaperlessLogo extends StatelessWidget {
|
||||||
|
static const _paperlessGreen = Color(0xFF18541F);
|
||||||
final double? height;
|
final double? height;
|
||||||
final double? width;
|
final double? width;
|
||||||
final String _path;
|
final Color _color;
|
||||||
|
|
||||||
const PaperlessLogo.white({super.key, this.height, this.width})
|
const PaperlessLogo.white({
|
||||||
: _path = "assets/logos/paperless_logo_white.svg";
|
super.key,
|
||||||
|
this.height,
|
||||||
|
this.width,
|
||||||
|
}) : _color = Colors.white;
|
||||||
|
|
||||||
const PaperlessLogo.green({super.key, this.height, this.width})
|
const PaperlessLogo.green({super.key, this.height, this.width})
|
||||||
: _path = "assets/logos/paperless_logo_green.svg";
|
: _color = _paperlessGreen;
|
||||||
|
|
||||||
const PaperlessLogo.black({super.key, this.height, this.width})
|
const PaperlessLogo.black({super.key, this.height, this.width})
|
||||||
: _path = "assets/logos/paperless_logo_black.svg";
|
: _color = Colors.black;
|
||||||
|
|
||||||
|
const PaperlessLogo.colored(Color color, {super.key, this.height, this.width})
|
||||||
|
: _color = color;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -24,7 +31,8 @@ class PaperlessLogo extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(right: 8),
|
padding: const EdgeInsets.only(right: 8),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
_path,
|
"assets/logos/paperless_logo_white.svg",
|
||||||
|
color: _color,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
21
lib/core/widgets/shimmer_placeholder.dart
Normal file
21
lib/core/widgets/shimmer_placeholder.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
|
class ShimmerPlaceholder extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const ShimmerPlaceholder({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Shimmer.fromColors(
|
||||||
|
baseColor: Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.grey[300]!
|
||||||
|
: Colors.grey[900]!,
|
||||||
|
highlightColor: Theme.of(context).brightness == Brightness.light
|
||||||
|
? Colors.grey[100]!
|
||||||
|
: Colors.grey[600]!,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extension DateComparisons on DateTime {
|
|
||||||
bool isEqualToIgnoringDate(DateTime other) {
|
|
||||||
return day == other.day && month == other.month && year == other.year;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
|
||||||
|
|
||||||
extension AddressableHydratedStorage on Storage {
|
extension AddressableHydratedStorage on Storage {
|
||||||
ApplicationSettingsState get settings {
|
ApplicationSettingsState get settings {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
|
||||||
|
|
||||||
extension ClientCertificateHandlingSecurityContext on SecurityContext {
|
|
||||||
SecurityContext withClientCertificate(ClientCertificate? clientCertificate) {
|
|
||||||
if (clientCertificate == null) return this;
|
|
||||||
return this
|
|
||||||
..usePrivateKeyBytes(
|
|
||||||
clientCertificate.bytes,
|
|
||||||
password: clientCertificate.passphrase,
|
|
||||||
)
|
|
||||||
..useCertificateChainBytes(
|
|
||||||
clientCertificate.bytes,
|
|
||||||
password: clientCertificate.passphrase,
|
|
||||||
)
|
|
||||||
..setTrustedCertificatesBytes(
|
|
||||||
clientCertificate.bytes,
|
|
||||||
password: clientCertificate.passphrase,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
extension SizeLimitedString on String {
|
|
||||||
String withLengthLimitedTo(int length, [String overflow = "..."]) {
|
|
||||||
return this.length > length
|
|
||||||
? '${substring(0, length - overflow.length)}$overflow'
|
|
||||||
: this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
117
lib/features/app_drawer/view/app_drawer.dart
Normal file
117
lib/features/app_drawer/view/app_drawer.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
import 'package:url_launcher/link.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class AppDrawer extends StatelessWidget {
|
||||||
|
const AppDrawer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: true,
|
||||||
|
child: Drawer(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const PaperlessLogo.green(),
|
||||||
|
Text(
|
||||||
|
"Paperless Mobile",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(S.of(context).appDrawerAboutLabel),
|
||||||
|
leading: const Icon(Icons.info_outline),
|
||||||
|
onTap: () => _showAboutDialog(context),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: const Icon(Icons.bug_report_outlined),
|
||||||
|
title: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString(
|
||||||
|
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: const Icon(Icons.settings_outlined),
|
||||||
|
title: Text(
|
||||||
|
S.of(context).appDrawerSettingsLabel,
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider.value(
|
||||||
|
value: context.read<ApplicationSettingsCubit>(),
|
||||||
|
child: const SettingsPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAboutDialog(BuildContext context) {
|
||||||
|
showAboutDialog(
|
||||||
|
context: context,
|
||||||
|
applicationIcon: const ImageIcon(
|
||||||
|
AssetImage('assets/logos/paperless_logo_green.png'),
|
||||||
|
),
|
||||||
|
applicationName: 'Paperless Mobile',
|
||||||
|
applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
|
||||||
|
children: [
|
||||||
|
Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')),
|
||||||
|
Link(
|
||||||
|
uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'),
|
||||||
|
builder: (context, followLink) => GestureDetector(
|
||||||
|
onTap: followLink,
|
||||||
|
child: Text(
|
||||||
|
'https://github.com/astubenbord/paperless-mobile',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Credits',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
_buildOnboardingImageCredits(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildOnboardingImageCredits() {
|
||||||
|
return Link(
|
||||||
|
uri: Uri.parse(
|
||||||
|
'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'),
|
||||||
|
builder: (context, followLink) => Wrap(
|
||||||
|
children: [
|
||||||
|
const Text('Onboarding images by '),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: followLink,
|
||||||
|
child: Text(
|
||||||
|
'pch.vector',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(' on Freepik.')
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class WelcomeIntroSlide extends StatelessWidget {
|
|
||||||
const WelcomeIntroSlide({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Welcome to Paperless Mobile!",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text(
|
|
||||||
"Manage, share and create documents on the go without any compromises!",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(color: Theme.of(context).hintColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(child: Image.asset("assets/logos/paperless_logo_green.png")),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,32 @@
|
|||||||
import 'dart:developer';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
|
|
||||||
part 'document_details_state.dart';
|
part 'document_details_state.dart';
|
||||||
|
|
||||||
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||||
final PaperlessDocumentsApi _api;
|
final PaperlessDocumentsApi _api;
|
||||||
|
final DocumentChangedNotifier _notifier;
|
||||||
|
|
||||||
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
: super(DocumentDetailsState(document: initialDocument)) {
|
DocumentDetailsCubit(
|
||||||
|
this._api,
|
||||||
|
this._notifier, {
|
||||||
|
required DocumentModel initialDocument,
|
||||||
|
}) : super(DocumentDetailsState(document: initialDocument)) {
|
||||||
|
_notifier.subscribe(this, onUpdated: replace);
|
||||||
loadSuggestions();
|
loadSuggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(DocumentModel document) async {
|
Future<void> delete(DocumentModel document) async {
|
||||||
await _api.delete(document);
|
await _api.delete(document);
|
||||||
|
_notifier.notifyDeleted(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadSuggestions() async {
|
Future<void> loadSuggestions() async {
|
||||||
@@ -44,21 +50,35 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
final int asn = await _api.findNextAsn();
|
final int asn = await _api.findNextAsn();
|
||||||
final updatedDocument =
|
final updatedDocument =
|
||||||
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
||||||
emit(state.copyWith(document: updatedDocument));
|
_notifier.notifyUpdated(updatedDocument);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ResultType> openDocumentInSystemViewer() async {
|
Future<ResultType> openDocumentInSystemViewer() async {
|
||||||
final downloadDir = await FileService.temporaryDirectory;
|
final cacheDir = await FileService.temporaryDirectory;
|
||||||
|
|
||||||
final metaData = await _api.getMetaData(state.document);
|
final metaData = await _api.getMetaData(state.document);
|
||||||
final docBytes = await _api.download(state.document);
|
final bytes = await _api.download(state.document);
|
||||||
File f = File('${downloadDir.path}/${metaData.mediaFilename}');
|
|
||||||
f.writeAsBytes(docBytes);
|
final file = File('${cacheDir.path}/${metaData.mediaFilename}')
|
||||||
return OpenFilex.open(f.path, type: "application/pdf")
|
..createSync(recursive: true)
|
||||||
.then((value) => value.type);
|
..writeAsBytesSync(bytes);
|
||||||
|
|
||||||
|
return OpenFilex.open(file.path, type: "application/pdf").then(
|
||||||
|
(value) => value.type,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void replaceDocument(DocumentModel document) {
|
void replace(DocumentModel document) {
|
||||||
emit(state.copyWith(document: document));
|
emit(state.copyWith(document: document));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
for (final element in _subscriptions) {
|
||||||
|
element.cancel();
|
||||||
|
}
|
||||||
|
_notifier.unsubscribe(this);
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
|
import 'package:badges/badges.dart' as b;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -8,7 +8,6 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||||
@@ -23,14 +22,15 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi
|
|||||||
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
|
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
|
||||||
|
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
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:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:badges/badges.dart' as b;
|
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.dart';
|
||||||
|
|
||||||
import '../../../../core/repository/state/impl/document_type_repository_state.dart';
|
|
||||||
|
|
||||||
|
//TODO: Refactor this into several widgets
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final bool allowEdit;
|
final bool allowEdit;
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
@@ -48,6 +48,16 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
|
late Future<DocumentMetaData> _metaData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_metaData = context
|
||||||
|
.read<PaperlessDocumentsApi>()
|
||||||
|
.getMetaData(context.read<DocumentDetailsCubit>().state.document);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
@@ -57,115 +67,15 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 4,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
|
||||||
floatingActionButton: widget.allowEdit
|
floatingActionButton: widget.allowEdit ? _buildAppBar() : null,
|
||||||
? BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
bottomNavigationBar: _buildBottomAppBar(),
|
||||||
builder: (context, state) {
|
|
||||||
final _filteredSuggestions =
|
|
||||||
state.suggestions.documentDifference(state.document);
|
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivityState) {
|
|
||||||
if (!connectivityState.isConnected) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return b.Badge(
|
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
|
||||||
showBadge: _filteredSuggestions.hasSuggestions,
|
|
||||||
child: Tooltip(
|
|
||||||
message:
|
|
||||||
S.of(context).documentDetailsPageEditTooltip,
|
|
||||||
preferBelow: false,
|
|
||||||
verticalOffset: 40,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.edit),
|
|
||||||
onPressed: () => _onEdit(state.document),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
badgeContent: Text(
|
|
||||||
'${_filteredSuggestions.suggestionsCount}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
badgeColor: Colors.red,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
bottomNavigationBar:
|
|
||||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return BottomAppBar(
|
|
||||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivityState) {
|
|
||||||
final isConnected = connectivityState.isConnected;
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPageDeleteTooltip,
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: widget.allowEdit && isConnected
|
|
||||||
? () => _onDelete(state.document)
|
|
||||||
: null,
|
|
||||||
).paddedSymmetrically(horizontal: 4),
|
|
||||||
Tooltip(
|
|
||||||
message:
|
|
||||||
S.of(context).documentDetailsPageDownloadTooltip,
|
|
||||||
child: DocumentDownloadButton(
|
|
||||||
document: state.document,
|
|
||||||
enabled: isConnected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPagePreviewTooltip,
|
|
||||||
icon: const Icon(Icons.visibility),
|
|
||||||
onPressed: isConnected
|
|
||||||
? () => _onOpen(state.document)
|
|
||||||
: null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
IconButton(
|
|
||||||
tooltip: S
|
|
||||||
.of(context)
|
|
||||||
.documentDetailsPageOpenInSystemViewerTooltip,
|
|
||||||
icon: const Icon(Icons.open_in_new),
|
|
||||||
onPressed:
|
|
||||||
isConnected ? _onOpenFileInSystemViewer : null,
|
|
||||||
).paddedOnly(right: 4.0),
|
|
||||||
IconButton(
|
|
||||||
tooltip:
|
|
||||||
S.of(context).documentDetailsPageShareTooltip,
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: isConnected
|
|
||||||
? () => _onShare(state.document)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
leading: IconButton(
|
leading: const BackButton(),
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors
|
|
||||||
.black, //TODO: check if there is a way to dynamically determine color...
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.of(context).pop(
|
|
||||||
context.read<DocumentDetailsCubit>().state.document,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 200.0,
|
expandedHeight: 200.0,
|
||||||
@@ -180,6 +90,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
tabBar: TabBar(
|
tabBar: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -208,6 +119,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
.onPrimaryContainer),
|
.onPrimaryContainer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
S
|
||||||
|
.of(context)
|
||||||
|
.documentDetailsPageTabSimilarDocumentsLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -215,19 +138,27 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
],
|
],
|
||||||
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return TabBarView(
|
return BlocProvider(
|
||||||
children: [
|
create: (context) => SimilarDocumentsCubit(
|
||||||
_buildDocumentOverview(
|
context.read(),
|
||||||
state.document,
|
context.read(),
|
||||||
),
|
documentId: state.document.id,
|
||||||
_buildDocumentContentView(
|
),
|
||||||
state.document,
|
child: TabBarView(
|
||||||
state,
|
children: [
|
||||||
),
|
_buildDocumentOverview(
|
||||||
_buildDocumentMetaDataView(
|
state.document,
|
||||||
state.document,
|
),
|
||||||
),
|
_buildDocumentContentView(
|
||||||
],
|
state.document,
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
_buildDocumentMetaDataView(
|
||||||
|
state.document,
|
||||||
|
),
|
||||||
|
const SimilarDocumentsView(),
|
||||||
|
],
|
||||||
|
),
|
||||||
).paddedSymmetrically(horizontal: 8);
|
).paddedSymmetrically(horizontal: 8);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -237,6 +168,94 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildAppBar() {
|
||||||
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final _filteredSuggestions =
|
||||||
|
state.suggestions.documentDifference(state.document);
|
||||||
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
|
builder: (context, connectivityState) {
|
||||||
|
if (!connectivityState.isConnected) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
return b.Badge(
|
||||||
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
|
showBadge: _filteredSuggestions.hasSuggestions,
|
||||||
|
child: Tooltip(
|
||||||
|
message: S.of(context).documentDetailsPageEditTooltip,
|
||||||
|
preferBelow: false,
|
||||||
|
verticalOffset: 40,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.edit),
|
||||||
|
onPressed: () => _onEdit(state.document),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
badgeContent: Text(
|
||||||
|
'${_filteredSuggestions.suggestionsCount}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
badgeColor: Colors.red,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() {
|
||||||
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return BottomAppBar(
|
||||||
|
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
|
builder: (context, connectivityState) {
|
||||||
|
final isConnected = connectivityState.isConnected;
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPageDeleteTooltip,
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: widget.allowEdit && isConnected
|
||||||
|
? () => _onDelete(state.document)
|
||||||
|
: null,
|
||||||
|
).paddedSymmetrically(horizontal: 4),
|
||||||
|
Tooltip(
|
||||||
|
message: S.of(context).documentDetailsPageDownloadTooltip,
|
||||||
|
child: DocumentDownloadButton(
|
||||||
|
document: state.document,
|
||||||
|
enabled: isConnected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPagePreviewTooltip,
|
||||||
|
icon: const Icon(Icons.visibility),
|
||||||
|
onPressed:
|
||||||
|
isConnected ? () => _onOpen(state.document) : null,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S
|
||||||
|
.of(context)
|
||||||
|
.documentDetailsPageOpenInSystemViewerTooltip,
|
||||||
|
icon: const Icon(Icons.open_in_new),
|
||||||
|
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
||||||
|
).paddedOnly(right: 4.0),
|
||||||
|
IconButton(
|
||||||
|
tooltip: S.of(context).documentDetailsPageShareTooltip,
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
onPressed:
|
||||||
|
isConnected ? () => _onShare(state.document) : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onEdit(DocumentModel document) async {
|
Future<void> _onEdit(DocumentModel document) async {
|
||||||
{
|
{
|
||||||
final cubit = context.read<DocumentDetailsCubit>();
|
final cubit = context.read<DocumentDetailsCubit>();
|
||||||
@@ -253,6 +272,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
documentTypeRepository: context.read(),
|
documentTypeRepository: context.read(),
|
||||||
storagePathRepository: context.read(),
|
storagePathRepository: context.read(),
|
||||||
tagRepository: context.read(),
|
tagRepository: context.read(),
|
||||||
|
notifier: context.read(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider<DocumentDetailsCubit>.value(
|
BlocProvider<DocumentDetailsCubit>.value(
|
||||||
@@ -263,7 +283,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.document != current.document,
|
previous.document != current.document,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
cubit.replaceDocument(state.document);
|
cubit.replace(state.document);
|
||||||
},
|
},
|
||||||
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -306,7 +326,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return FutureBuilder<DocumentMetaData>(
|
return FutureBuilder<DocumentMetaData>(
|
||||||
future: context.read<PaperlessDocumentsApi>().getMetaData(document),
|
future: _metaData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) {
|
if (!snapshot.hasData) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -430,7 +450,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
visible: document.documentType != null,
|
visible: document.documentType != null,
|
||||||
child: _DetailsItem(
|
child: _DetailsItem(
|
||||||
label: S.of(context).documentDocumentTypePropertyLabel,
|
label: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
content: LabelText<DocumentType, DocumentTypeRepositoryState>(
|
content: LabelText<DocumentType>(
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
id: document.documentType,
|
id: document.documentType,
|
||||||
),
|
),
|
||||||
@@ -440,7 +460,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
visible: document.correspondent != null,
|
visible: document.correspondent != null,
|
||||||
child: _DetailsItem(
|
child: _DetailsItem(
|
||||||
label: S.of(context).documentCorrespondentPropertyLabel,
|
label: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
content: LabelText<Correspondent, CorrespondentRepositoryState>(
|
content: LabelText<Correspondent>(
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
id: document.correspondent,
|
id: document.correspondent,
|
||||||
),
|
),
|
||||||
@@ -451,7 +471,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
child: _DetailsItem(
|
child: _DetailsItem(
|
||||||
label: S.of(context).documentStoragePathPropertyLabel,
|
label: S.of(context).documentStoragePathPropertyLabel,
|
||||||
content: StoragePathWidget(
|
content: StoragePathWidget(
|
||||||
isClickable: widget.isLabelClickable,
|
|
||||||
pathId: document.storagePath,
|
pathId: document.storagePath,
|
||||||
),
|
),
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
@@ -465,34 +484,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
child: TagsWidget(
|
child: TagsWidget(
|
||||||
isClickable: widget.isLabelClickable,
|
isClickable: widget.isLabelClickable,
|
||||||
tagIds: document.tags,
|
tagIds: document.tags,
|
||||||
onTagSelected: (int tagId) {},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddedSymmetrically(vertical: 16),
|
).paddedSymmetrically(vertical: 16),
|
||||||
),
|
),
|
||||||
// _separator(),
|
|
||||||
// FutureBuilder<List<SimilarDocumentModel>>(
|
|
||||||
// future: getIt<DocumentRepository>().findSimilar(document.id),
|
|
||||||
// builder: (context, snapshot) {
|
|
||||||
// if (!snapshot.hasData) {
|
|
||||||
// return CircularProgressIndicator();
|
|
||||||
// }
|
|
||||||
// return ExpansionTile(
|
|
||||||
// tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
// title: Text(
|
|
||||||
// S.of(context).documentDetailsPageSimilarDocumentsLabel,
|
|
||||||
// style:
|
|
||||||
// Theme.of(context).textTheme.headline5?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
// ),
|
|
||||||
// children: snapshot.data!
|
|
||||||
// .map((e) => DocumentListItem(
|
|
||||||
// document: e,
|
|
||||||
// onTap: (doc) {},
|
|
||||||
// isSelected: false,
|
|
||||||
// isAtLeastOneSelected: false))
|
|
||||||
// .toList(),
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -549,15 +544,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String formatBytes(int bytes, int decimals) {
|
|
||||||
if (bytes <= 0) return "0 B";
|
|
||||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
||||||
var i = (log(bytes) / log(1024)).floor();
|
|
||||||
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) +
|
|
||||||
' ' +
|
|
||||||
suffixes[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DetailsItem extends StatelessWidget {
|
class _DetailsItem extends StatelessWidget {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class DocumentDownloadButton extends StatefulWidget {
|
class DocumentDownloadButton extends StatefulWidget {
|
||||||
@@ -47,20 +48,24 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
|
final service = context.read<PaperlessDocumentsApi>();
|
||||||
try {
|
try {
|
||||||
final bytes =
|
final bytes = await service.download(document);
|
||||||
await context.read<PaperlessDocumentsApi>().download(document);
|
final meta = await service.getMetaData(document);
|
||||||
final Directory dir = await FileService.downloadsDirectory;
|
final Directory dir = await FileService.downloadsDirectory;
|
||||||
String filePath = "${dir.path}/${document.originalFileName}";
|
String filePath = "${dir.path}/${meta.mediaFilename}";
|
||||||
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
|
final createdFile = File(filePath);
|
||||||
await File(filePath).writeAsBytes(bytes);
|
createdFile.createSync(recursive: true);
|
||||||
|
createdFile.writeAsBytesSync(bytes);
|
||||||
showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
|
showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showGenericError(context, error);
|
showGenericError(context, error);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isDownloadPending = false);
|
if (mounted) {
|
||||||
|
setState(() => _isDownloadPending = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||||
|
|
||||||
|
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||||
|
with PagedDocumentsMixin {
|
||||||
|
@override
|
||||||
|
final PaperlessDocumentsApi api;
|
||||||
|
@override
|
||||||
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
|
DocumentSearchCubit(this.api, this.notifier)
|
||||||
|
: super(const DocumentSearchState()) {
|
||||||
|
notifier.subscribe(
|
||||||
|
this,
|
||||||
|
onDeleted: remove,
|
||||||
|
onUpdated: replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> search(String query) async {
|
||||||
|
emit(state.copyWith(
|
||||||
|
isLoading: true,
|
||||||
|
suggestions: [],
|
||||||
|
view: SearchView.results,
|
||||||
|
));
|
||||||
|
final searchFilter = DocumentFilter(
|
||||||
|
query: TextQuery.titleAndContent(query),
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateFilter(filter: searchFilter);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
searchHistory: [
|
||||||
|
query,
|
||||||
|
...state.searchHistory
|
||||||
|
.whereNot((previousQuery) => previousQuery == query)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> suggest(String query) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: true,
|
||||||
|
view: SearchView.suggestions,
|
||||||
|
value: [],
|
||||||
|
suggestions: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final suggestions = await api.autocomplete(query);
|
||||||
|
emit(state.copyWith(
|
||||||
|
suggestions: suggestions,
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
emit(state.copyWith(
|
||||||
|
view: SearchView.suggestions,
|
||||||
|
suggestions: [],
|
||||||
|
isLoading: false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
notifier.unsubscribe(this);
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DocumentSearchState? fromJson(Map<String, dynamic> json) {
|
||||||
|
return DocumentSearchState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? toJson(DocumentSearchState state) {
|
||||||
|
return state.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
|
|
||||||
|
part 'document_search_state.g.dart';
|
||||||
|
|
||||||
|
enum SearchView {
|
||||||
|
suggestions,
|
||||||
|
results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable(ignoreUnannotated: true)
|
||||||
|
class DocumentSearchState extends PagedDocumentsState {
|
||||||
|
@JsonKey()
|
||||||
|
final List<String> searchHistory;
|
||||||
|
final SearchView view;
|
||||||
|
final List<String> suggestions;
|
||||||
|
const DocumentSearchState({
|
||||||
|
this.view = SearchView.suggestions,
|
||||||
|
this.searchHistory = const [],
|
||||||
|
this.suggestions = const [],
|
||||||
|
super.filter,
|
||||||
|
super.hasLoaded,
|
||||||
|
super.isLoading,
|
||||||
|
super.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
...super.props,
|
||||||
|
searchHistory,
|
||||||
|
suggestions,
|
||||||
|
view,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
DocumentSearchState copyWithPaged({
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
}) {
|
||||||
|
return copyWith(
|
||||||
|
hasLoaded: hasLoaded,
|
||||||
|
isLoading: isLoading,
|
||||||
|
filter: filter,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentSearchState copyWith({
|
||||||
|
List<String>? searchHistory,
|
||||||
|
bool? hasLoaded,
|
||||||
|
bool? isLoading,
|
||||||
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
|
DocumentFilter? filter,
|
||||||
|
List<String>? suggestions,
|
||||||
|
SearchView? view,
|
||||||
|
}) {
|
||||||
|
return DocumentSearchState(
|
||||||
|
value: value ?? this.value,
|
||||||
|
filter: filter ?? this.filter,
|
||||||
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
searchHistory: searchHistory ?? this.searchHistory,
|
||||||
|
view: view ?? this.view,
|
||||||
|
suggestions: suggestions ?? this.suggestions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory DocumentSearchState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DocumentSearchStateFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$DocumentSearchStateToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'document_search_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
DocumentSearchState _$DocumentSearchStateFromJson(Map<String, dynamic> json) =>
|
||||||
|
DocumentSearchState(
|
||||||
|
searchHistory: (json['searchHistory'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$DocumentSearchStateToJson(
|
||||||
|
DocumentSearchState instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'searchHistory': instance.searchHistory,
|
||||||
|
};
|
||||||
182
lib/features/document_search/view/document_search_page.dart
Normal file
182
lib/features/document_search/view/document_search_page.dart
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||||
|
|
||||||
|
Future<void> showDocumentSearchPage(BuildContext context) {
|
||||||
|
return Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider(
|
||||||
|
create: (context) => DocumentSearchCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
),
|
||||||
|
child: const DocumentSearchPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentSearchPage extends StatefulWidget {
|
||||||
|
const DocumentSearchPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentSearchPage> createState() => _DocumentSearchPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
||||||
|
final _queryController = TextEditingController(text: '');
|
||||||
|
|
||||||
|
String get query => _queryController.text;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
toolbarHeight: 72,
|
||||||
|
leading: BackButton(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
style: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
hintText: S.of(context).documentSearchSearchDocuments,
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
controller: _queryController,
|
||||||
|
onChanged: context.read<DocumentSearchCubit>().suggest,
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onSubmitted: (query) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
context.read<DocumentSearchCubit>().search(query);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<DocumentSearchCubit>().reset();
|
||||||
|
_queryController.clear();
|
||||||
|
},
|
||||||
|
).padded(),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Divider(
|
||||||
|
color: theme.colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
switch (state.view) {
|
||||||
|
case SearchView.suggestions:
|
||||||
|
return _buildSuggestionsView(state);
|
||||||
|
case SearchView.results:
|
||||||
|
return _buildResultsView(state);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuggestionsView(DocumentSearchState state) {
|
||||||
|
final suggestions = state.suggestions
|
||||||
|
.whereNot((element) => state.searchHistory.contains(element))
|
||||||
|
.toList();
|
||||||
|
final historyMatches = state.searchHistory
|
||||||
|
.where(
|
||||||
|
(element) => element.startsWith(query),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => ListTile(
|
||||||
|
title: Text(historyMatches[index]),
|
||||||
|
leading: const Icon(Icons.history),
|
||||||
|
onTap: () => _selectSuggestion(historyMatches[index]),
|
||||||
|
),
|
||||||
|
childCount: historyMatches.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.isLoading)
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => ListTile(
|
||||||
|
title: Text(suggestions[index]),
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
onTap: () => _selectSuggestion(suggestions[index]),
|
||||||
|
),
|
||||||
|
childCount: suggestions.length,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildResultsView(DocumentSearchState state) {
|
||||||
|
final header = Text(
|
||||||
|
S.of(context).documentSearchResults,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
).padded();
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(child: header),
|
||||||
|
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Text(S.of(context).documentSearchNoMatchesFound),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverAdaptiveDocumentsView(
|
||||||
|
documents: state.documents,
|
||||||
|
hasInternetConnection: true,
|
||||||
|
isLabelClickable: false,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
enableHeroAnimation: false,
|
||||||
|
onTap: (document) {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
DocumentDetailsRoute.routeName,
|
||||||
|
arguments: DocumentDetailsRouteArguments(
|
||||||
|
document: document,
|
||||||
|
isLabelClickable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectSuggestion(String suggestion) {
|
||||||
|
_queryController.text = suggestion;
|
||||||
|
context.read<DocumentSearchCubit>().search(suggestion);
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,29 +8,23 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
|
||||||
|
|
||||||
part 'document_upload_state.dart';
|
part 'document_upload_state.dart';
|
||||||
|
|
||||||
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||||
final PaperlessDocumentsApi _documentApi;
|
final PaperlessDocumentsApi _documentApi;
|
||||||
|
|
||||||
final LabelRepository<Tag, TagRepositoryState> _tagRepository;
|
final LabelRepository<Tag> _tagRepository;
|
||||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
final LabelRepository<Correspondent> _correspondentRepository;
|
||||||
_correspondentRepository;
|
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||||
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
|
||||||
_documentTypeRepository;
|
|
||||||
|
|
||||||
final List<StreamSubscription> _subs = [];
|
final List<StreamSubscription> _subs = [];
|
||||||
|
|
||||||
DocumentUploadCubit({
|
DocumentUploadCubit({
|
||||||
required LocalVault localVault,
|
|
||||||
required PaperlessDocumentsApi documentApi,
|
required PaperlessDocumentsApi documentApi,
|
||||||
required LabelRepository<Tag, TagRepositoryState> tagRepository,
|
required LabelRepository<Tag> tagRepository,
|
||||||
required LabelRepository<Correspondent, CorrespondentRepositoryState>
|
required LabelRepository<Correspondent> correspondentRepository,
|
||||||
correspondentRepository,
|
required LabelRepository<DocumentType> documentTypeRepository,
|
||||||
required LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
|
||||||
documentTypeRepository,
|
|
||||||
}) : _documentApi = documentApi,
|
}) : _documentApi = documentApi,
|
||||||
_tagRepository = tagRepository,
|
_tagRepository = tagRepository,
|
||||||
_correspondentRepository = correspondentRepository,
|
_correspondentRepository = correspondentRepository,
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/type/types.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||||
@@ -19,7 +16,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type
|
|||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
class DocumentUploadPreparationPage extends StatefulWidget {
|
class DocumentUploadPreparationPage extends StatefulWidget {
|
||||||
final Uint8List fileBytes;
|
final Uint8List fileBytes;
|
||||||
@@ -172,9 +169,8 @@ class _DocumentUploadPreparationPageState
|
|||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialName) =>
|
labelCreationWidgetBuilder: (initialName) =>
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) =>
|
||||||
LabelRepository<DocumentType,
|
context.read<LabelRepository<DocumentType>>(),
|
||||||
DocumentTypeRepositoryState>>(),
|
|
||||||
child: AddDocumentTypePage(initialName: initialName),
|
child: AddDocumentTypePage(initialName: initialName),
|
||||||
),
|
),
|
||||||
textFieldLabel:
|
textFieldLabel:
|
||||||
@@ -188,9 +184,8 @@ class _DocumentUploadPreparationPageState
|
|||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialName) =>
|
labelCreationWidgetBuilder: (initialName) =>
|
||||||
RepositoryProvider(
|
RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) =>
|
||||||
LabelRepository<Correspondent,
|
context.read<LabelRepository<Correspondent>>(),
|
||||||
CorrespondentRepositoryState>>(),
|
|
||||||
child: AddCorrespondentPage(initialName: initialName),
|
child: AddCorrespondentPage(initialName: initialName),
|
||||||
),
|
),
|
||||||
textFieldLabel:
|
textFieldLabel:
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
|
import 'package:paperless_mobile/features/paged_document_view/paged_documents_mixin.dart';
|
||||||
|
|
||||||
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||||
with DocumentsPagingMixin {
|
with PagedDocumentsMixin {
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
final SavedViewRepository _savedViewRepository;
|
@override
|
||||||
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
DocumentsCubit(this.api, this._savedViewRepository)
|
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
|
||||||
: super(const DocumentsState());
|
notifier.subscribe(
|
||||||
|
this,
|
||||||
|
onUpdated: replace,
|
||||||
|
onDeleted: remove,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> bulkRemove(List<DocumentModel> documents) async {
|
Future<void> bulkDelete(List<DocumentModel> documents) async {
|
||||||
log("[DocumentsCubit] bulkRemove");
|
debugPrint("[DocumentsCubit] bulkRemove");
|
||||||
await api.bulkAction(
|
await api.bulkAction(
|
||||||
BulkDeleteAction(documents.map((doc) => doc.id)),
|
BulkDeleteAction(documents.map((doc) => doc.id)),
|
||||||
);
|
);
|
||||||
|
for (final deletedDoc in documents) {
|
||||||
|
notifier.notifyDeleted(deletedDoc);
|
||||||
|
}
|
||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +40,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
Iterable<int> addTags = const [],
|
Iterable<int> addTags = const [],
|
||||||
Iterable<int> removeTags = const [],
|
Iterable<int> removeTags = const [],
|
||||||
}) async {
|
}) async {
|
||||||
log("[DocumentsCubit] bulkEditTags");
|
debugPrint("[DocumentsCubit] bulkEditTags");
|
||||||
await api.bulkAction(BulkModifyTagsAction(
|
await api.bulkAction(BulkModifyTagsAction(
|
||||||
documents.map((doc) => doc.id),
|
documents.map((doc) => doc.id),
|
||||||
addTags: addTags,
|
addTags: addTags,
|
||||||
@@ -40,7 +50,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void toggleDocumentSelection(DocumentModel model) {
|
void toggleDocumentSelection(DocumentModel model) {
|
||||||
log("[DocumentsCubit] toggleSelection");
|
debugPrint("[DocumentsCubit] toggleSelection");
|
||||||
if (state.selectedIds.contains(model.id)) {
|
if (state.selectedIds.contains(model.id)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@@ -50,54 +60,25 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(state.copyWith(selection: [...state.selection, model]));
|
||||||
state.copyWith(selection: [...state.selection, model]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetSelection() {
|
void resetSelection() {
|
||||||
log("[DocumentsCubit] resetSelection");
|
debugPrint("[DocumentsCubit] resetSelection");
|
||||||
emit(state.copyWith(selection: []));
|
emit(state.copyWith(selection: []));
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
log("[DocumentsCubit] reset");
|
debugPrint("[DocumentsCubit] reset");
|
||||||
emit(const DocumentsState());
|
emit(const DocumentsState());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectView(int id) async {
|
|
||||||
emit(state.copyWith(isLoading: true));
|
|
||||||
try {
|
|
||||||
final filter =
|
|
||||||
_savedViewRepository.current?.values[id]?.toDocumentFilter();
|
|
||||||
if (filter == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final results = await api.findAll(filter.copyWith(page: 1));
|
|
||||||
emit(
|
|
||||||
DocumentsState(
|
|
||||||
filter: filter,
|
|
||||||
hasLoaded: true,
|
|
||||||
isLoading: false,
|
|
||||||
selectedSavedViewId: id,
|
|
||||||
value: [results],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
emit(state.copyWith(isLoading: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Iterable<String>> autocomplete(String query) async {
|
Future<Iterable<String>> autocomplete(String query) async {
|
||||||
final res = await api.autocomplete(query);
|
final res = await api.autocomplete(query);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void unselectView() {
|
|
||||||
emit(state.copyWith(selectedSavedViewId: () => null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DocumentsState? fromJson(Map<String, dynamic> json) {
|
DocumentsState? fromJson(Map<String, dynamic> json) {
|
||||||
return DocumentsState.fromJson(json);
|
return DocumentsState.fromJson(json);
|
||||||
@@ -107,4 +88,10 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
Map<String, dynamic>? toJson(DocumentsState state) {
|
Map<String, dynamic>? toJson(DocumentsState state) {
|
||||||
return state.toJson();
|
return state.toJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
notifier.unsubscribe(this);
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
|
|
||||||
class DocumentsState extends DocumentsPagedState {
|
class DocumentsState extends PagedDocumentsState {
|
||||||
final int? selectedSavedViewId;
|
@JsonKey(includeFromJson: true, includeToJson: false)
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
|
||||||
final List<DocumentModel> selection;
|
final List<DocumentModel> selection;
|
||||||
|
|
||||||
const DocumentsState({
|
const DocumentsState({
|
||||||
this.selection = const [],
|
this.selection = const [],
|
||||||
this.selectedSavedViewId,
|
|
||||||
super.value = const [],
|
super.value = const [],
|
||||||
super.filter = const DocumentFilter(),
|
super.filter = const DocumentFilter(),
|
||||||
super.hasLoaded = false,
|
super.hasLoaded = false,
|
||||||
@@ -25,7 +22,6 @@ class DocumentsState extends DocumentsPagedState {
|
|||||||
List<PagedSearchResult<DocumentModel>>? value,
|
List<PagedSearchResult<DocumentModel>>? value,
|
||||||
DocumentFilter? filter,
|
DocumentFilter? filter,
|
||||||
List<DocumentModel>? selection,
|
List<DocumentModel>? selection,
|
||||||
int? Function()? selectedSavedViewId,
|
|
||||||
}) {
|
}) {
|
||||||
return DocumentsState(
|
return DocumentsState(
|
||||||
hasLoaded: hasLoaded ?? this.hasLoaded,
|
hasLoaded: hasLoaded ?? this.hasLoaded,
|
||||||
@@ -33,20 +29,13 @@ class DocumentsState extends DocumentsPagedState {
|
|||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
filter: filter ?? this.filter,
|
filter: filter ?? this.filter,
|
||||||
selection: selection ?? this.selection,
|
selection: selection ?? this.selection,
|
||||||
selectedSavedViewId: selectedSavedViewId != null
|
|
||||||
? selectedSavedViewId.call()
|
|
||||||
: this.selectedSavedViewId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
hasLoaded,
|
|
||||||
filter,
|
|
||||||
value,
|
|
||||||
selection,
|
selection,
|
||||||
isLoading,
|
...super.props,
|
||||||
selectedSavedViewId,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -54,7 +43,6 @@ class DocumentsState extends DocumentsPagedState {
|
|||||||
'hasLoaded': hasLoaded,
|
'hasLoaded': hasLoaded,
|
||||||
'isLoading': isLoading,
|
'isLoading': isLoading,
|
||||||
'filter': filter.toJson(),
|
'filter': filter.toJson(),
|
||||||
'selectedSavedViewId': selectedSavedViewId,
|
|
||||||
'value':
|
'value':
|
||||||
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
|
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
|
||||||
};
|
};
|
||||||
@@ -65,7 +53,6 @@ class DocumentsState extends DocumentsPagedState {
|
|||||||
return DocumentsState(
|
return DocumentsState(
|
||||||
hasLoaded: json['hasLoaded'],
|
hasLoaded: json['hasLoaded'],
|
||||||
isLoading: json['isLoading'],
|
isLoading: json['isLoading'],
|
||||||
selectedSavedViewId: json['selectedSavedViewId'],
|
|
||||||
value: (json['value'] as List<dynamic>)
|
value: (json['value'] as List<dynamic>)
|
||||||
.map((e) =>
|
.map((e) =>
|
||||||
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
|
PagedSearchResult.fromJsonT(e, DocumentModelJsonConverter()))
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_
|
|||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
|
||||||
class DocumentEditPage extends StatefulWidget {
|
class DocumentEditPage extends StatefulWidget {
|
||||||
final FieldSuggestions suggestions;
|
final FieldSuggestions suggestions;
|
||||||
@@ -159,8 +160,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) => context.read<LabelRepository<StoragePath>>(),
|
||||||
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
|
||||||
child: AddStoragePathPage(initalValue: initialValue),
|
child: AddStoragePathPage(initalValue: initialValue),
|
||||||
),
|
),
|
||||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||||
@@ -181,8 +181,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) => context.read<LabelRepository<Correspondent>>(),
|
||||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
|
||||||
child: AddCorrespondentPage(initialName: initialValue),
|
child: AddCorrespondentPage(initialName: initialValue),
|
||||||
),
|
),
|
||||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
@@ -214,8 +213,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
notAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
formBuilderState: _formKey.currentState,
|
formBuilderState: _formKey.currentState,
|
||||||
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
||||||
create: (context) => context.read<
|
create: (context) => context.read<LabelRepository<DocumentType>>(),
|
||||||
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
|
||||||
child: AddDocumentTypePage(
|
child: AddDocumentTypePage(
|
||||||
initialName: currentInput,
|
initialName: currentInput,
|
||||||
),
|
),
|
||||||
@@ -290,7 +288,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||||
),
|
),
|
||||||
initialValue: initialCreatedAtDate,
|
initialValue: initialCreatedAtDate,
|
||||||
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
|
format: DateFormat.yMMMMd(),
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
initialEntryMode: DatePickerEntryMode.calendar,
|
||||||
),
|
),
|
||||||
if (_filteredSuggestions.hasSuggestedDates)
|
if (_filteredSuggestions.hasSuggestedDates)
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:badges/badges.dart' as b;
|
import 'package:badges/badges.dart' as b;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/app_drawer.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
|
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_list.dart';
|
||||||
|
import 'package:paperless_mobile/features/search_app_bar/view/search_app_bar.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
final DocumentFilter? filter;
|
final DocumentFilter? filter;
|
||||||
@@ -38,6 +39,7 @@ class DocumentFilterIntent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Refactor this
|
||||||
class DocumentsPage extends StatefulWidget {
|
class DocumentsPage extends StatefulWidget {
|
||||||
const DocumentsPage({Key? key}) : super(key: key);
|
const DocumentsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -45,52 +47,38 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
State<DocumentsPage> createState() => _DocumentsPageState();
|
State<DocumentsPage> createState() => _DocumentsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage> {
|
class _DocumentsPageState extends State<DocumentsPage>
|
||||||
final ScrollController _scrollController = ScrollController();
|
with SingleTickerProviderStateMixin {
|
||||||
double _offset = 0;
|
late final TabController _tabController;
|
||||||
double _last = 0;
|
|
||||||
|
|
||||||
static const double _savedViewWidgetHeight = 80 + 16;
|
int _currentTab = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: 0,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload();
|
||||||
context.read<SavedViewCubit>().reload();
|
context.read<SavedViewCubit>().reload();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
_scrollController
|
_tabController.addListener(_listenForTabChanges);
|
||||||
..addListener(_listenForScrollChanges)
|
|
||||||
..addListener(_listenForLoadNewData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenForLoadNewData() {
|
void _listenForTabChanges() {
|
||||||
final currState = context.read<DocumentsCubit>().state;
|
setState(() {
|
||||||
if (_scrollController.offset >=
|
_currentTab = _tabController.index;
|
||||||
_scrollController.position.maxScrollExtent * 0.75 &&
|
});
|
||||||
!currState.isLoading &&
|
|
||||||
!currState.isLastPageLoaded) {
|
|
||||||
_loadNewPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _listenForScrollChanges() {
|
|
||||||
final current = _scrollController.offset;
|
|
||||||
_offset += _last - current;
|
|
||||||
|
|
||||||
if (_offset <= -_savedViewWidgetHeight) _offset = -_savedViewWidgetHeight;
|
|
||||||
if (_offset >= 0) _offset = 0;
|
|
||||||
_last = current;
|
|
||||||
if (_offset <= 0 && _offset >= -_savedViewWidgetHeight) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,77 +115,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
const linearProgressIndicatorHeight = 4.0;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: BlocProvider.value(
|
drawer: const AppDrawer(),
|
||||||
value: context.read<AuthenticationCubit>(),
|
|
||||||
child: AppDrawer(
|
|
||||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(
|
|
||||||
kToolbarHeight + linearProgressIndicatorHeight,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.selection.isEmpty) {
|
|
||||||
return AppBar(
|
|
||||||
title: Text(
|
|
||||||
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
const SortDocumentsButton(),
|
|
||||||
BlocBuilder<ApplicationSettingsCubit,
|
|
||||||
ApplicationSettingsState>(
|
|
||||||
builder: (context, settingsState) => IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
settingsState.preferredViewType == ViewType.grid
|
|
||||||
? Icons.list
|
|
||||||
: Icons.grid_view_rounded,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
|
||||||
setState(() {
|
|
||||||
_offset = 0;
|
|
||||||
_last = 0;
|
|
||||||
});
|
|
||||||
final cubit =
|
|
||||||
context.read<ApplicationSettingsCubit>();
|
|
||||||
cubit.setViewType(
|
|
||||||
cubit.state.preferredViewType.toggle());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(
|
|
||||||
linearProgressIndicatorHeight),
|
|
||||||
child: state.isLoading && state.hasLoaded
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: const SizedBox(height: 4.0),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<DocumentsCubit>().resetSelection(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () => _onDelete(context, state),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
@@ -212,10 +131,15 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
),
|
),
|
||||||
animationType: b.BadgeAnimationType.fade,
|
animationType: b.BadgeAnimationType.fade,
|
||||||
badgeColor: Colors.red,
|
badgeColor: Colors.red,
|
||||||
child: FloatingActionButton(
|
child: _currentTab == 0
|
||||||
child: const Icon(Icons.filter_alt_outlined),
|
? FloatingActionButton(
|
||||||
onPressed: _openDocumentFilter,
|
child: const Icon(Icons.filter_alt_outlined),
|
||||||
),
|
onPressed: _openDocumentFilter,
|
||||||
|
)
|
||||||
|
: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
onPressed: () => _onCreateSavedView(state.filter),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -227,35 +151,200 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: RefreshIndicator(
|
child: NestedScrollView(
|
||||||
onRefresh: _onRefresh,
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
notificationPredicate: (_) => connectivityState.isConnected,
|
SliverOverlapAbsorber(
|
||||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
// This widget takes the overlapping behavior of the SliverAppBar,
|
||||||
builder: (context, taskState) {
|
// and redirects it to the SliverOverlapInjector below. If it is
|
||||||
return Stack(
|
// missing, then it is possible for the nested "inner" scroll view
|
||||||
children: [
|
// below to end up under the SliverAppBar even when the inner
|
||||||
_buildBody(connectivityState),
|
// scroll view thinks it has not been scrolled.
|
||||||
Positioned(
|
// This is not necessary if the "headerSliverBuilder" only builds
|
||||||
left: 0,
|
// widgets that do not overlap the next sliver.
|
||||||
right: 0,
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
top: _offset,
|
context,
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
),
|
||||||
builder: (context, state) {
|
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
return ColoredBox(
|
builder: (context, state) {
|
||||||
color: Theme.of(context).colorScheme.background,
|
if (state.selection.isNotEmpty) {
|
||||||
child: SavedViewSelectionWidget(
|
return SliverAppBar(
|
||||||
height: _savedViewWidgetHeight,
|
floating: false,
|
||||||
currentFilter: state.filter,
|
pinned: true,
|
||||||
enabled: state.selection.isEmpty &&
|
leading: IconButton(
|
||||||
connectivityState.isConnected,
|
icon: const Icon(Icons.close),
|
||||||
),
|
onPressed: () => context
|
||||||
);
|
.read<DocumentsCubit>()
|
||||||
},
|
.resetSelection(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"${state.selection.length} ${S.of(context).documentsSelectedText}",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () => _onDelete(state),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SearchAppBar(
|
||||||
|
hintText: S.of(context).documentSearchSearchDocuments,
|
||||||
|
onOpenSearch: showDocumentSearchPage,
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: S.of(context).documentsPageTitle),
|
||||||
|
Tab(text: S.of(context).savedViewsLabel),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
final metrics = notification.metrics;
|
||||||
|
if (metrics.maxScrollExtent == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final desiredTab =
|
||||||
|
(metrics.pixels / metrics.maxScrollExtent).round();
|
||||||
|
if (metrics.axis == Axis.horizontal &&
|
||||||
|
_currentTab != desiredTab) {
|
||||||
|
setState(() => _currentTab = desiredTab);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: NotificationListener<ScrollMetricsNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
// Listen for scroll notifications to load new data.
|
||||||
|
// Scroll controller does not work here due to nestedscrollview limitations.
|
||||||
|
final currState = context.read<DocumentsCubit>().state;
|
||||||
|
final max = notification.metrics.maxScrollExtent;
|
||||||
|
if (max == 0 ||
|
||||||
|
_currentTab != 0 ||
|
||||||
|
currState.isLoading ||
|
||||||
|
currState.isLastPageLoaded) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final offset = notification.metrics.pixels;
|
||||||
|
if (offset >= max * 0.7) {
|
||||||
|
context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.loadMore()
|
||||||
|
.onError<PaperlessServerException>(
|
||||||
|
(error, stackTrace) => showErrorMessage(
|
||||||
|
context,
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||||
|
onRefresh: _onReloadDocuments,
|
||||||
|
notificationPredicate: (_) =>
|
||||||
|
connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("documents"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView
|
||||||
|
.sliverOverlapAbsorberHandleFor(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
_buildViewActions(),
|
||||||
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
// 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) {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: DocumentsEmptyState(
|
||||||
|
state: state,
|
||||||
|
onReset: () {
|
||||||
|
context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.resetFilter();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return BlocBuilder<
|
||||||
|
ApplicationSettingsCubit,
|
||||||
|
ApplicationSettingsState>(
|
||||||
|
builder: (context, settings) {
|
||||||
|
return SliverAdaptiveDocumentsView(
|
||||||
|
viewType:
|
||||||
|
settings.preferredViewType,
|
||||||
|
onTap: _openDetails,
|
||||||
|
onSelected: context
|
||||||
|
.read<DocumentsCubit>()
|
||||||
|
.toggleDocumentSelection,
|
||||||
|
hasInternetConnection:
|
||||||
|
connectivityState.isConnected,
|
||||||
|
onTagSelected: _addTagToFilter,
|
||||||
|
onCorrespondentSelected:
|
||||||
|
_addCorrespondentToFilter,
|
||||||
|
onDocumentTypeSelected:
|
||||||
|
_addDocumentTypeToFilter,
|
||||||
|
onStoragePathSelected:
|
||||||
|
_addStoragePathToFilter,
|
||||||
|
documents: state.documents,
|
||||||
|
hasLoaded: state.hasLoaded,
|
||||||
|
isLabelClickable: true,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
selectedDocumentIds:
|
||||||
|
state.selectedIds,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
edgeOffset: kToolbarHeight + kTextTabBarHeight,
|
||||||
|
onRefresh: _onReloadSavedViews,
|
||||||
|
notificationPredicate: (_) =>
|
||||||
|
connectivityState.isConnected,
|
||||||
|
child: CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>("savedViews"),
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverOverlapInjector(
|
||||||
|
handle: NestedScrollView
|
||||||
|
.sliverOverlapAbsorberHandleFor(
|
||||||
|
context),
|
||||||
|
),
|
||||||
|
const SavedViewList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -265,7 +354,33 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
Widget _buildViewActions() {
|
||||||
|
return SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SortDocumentsButton(),
|
||||||
|
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
state.preferredViewType == ViewType.list
|
||||||
|
? Icons.grid_view_rounded
|
||||||
|
: Icons.list,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<ApplicationSettingsCubit>().setViewType(
|
||||||
|
state.preferredViewType.toggle(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddedSymmetrically(horizontal: 8, vertical: 4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDelete(DocumentsState documentsState) async {
|
||||||
final shouldDelete = await showDialog<bool>(
|
final shouldDelete = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
@@ -276,7 +391,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
try {
|
try {
|
||||||
await context
|
await context
|
||||||
.read<DocumentsCubit>()
|
.read<DocumentsCubit>()
|
||||||
.bulkRemove(documentsState.selection);
|
.bulkDelete(documentsState.selection);
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
context,
|
context,
|
||||||
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
||||||
@@ -288,6 +403,25 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCreateSavedView(DocumentFilter filter) async {
|
||||||
|
final newView = await Navigator.of(context).push<SavedView?>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => LabelsBlocProvider(
|
||||||
|
child: AddSavedViewPage(
|
||||||
|
currentFilter: filter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (newView != null) {
|
||||||
|
try {
|
||||||
|
await context.read<SavedViewCubit>().add(newView);
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _openDocumentFilter() async {
|
void _openDocumentFilter() async {
|
||||||
final draggableSheetController = DraggableScrollableController();
|
final draggableSheetController = DraggableScrollableController();
|
||||||
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
||||||
@@ -323,12 +457,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
try {
|
try {
|
||||||
if (filterIntent.shouldReset) {
|
if (filterIntent.shouldReset) {
|
||||||
await context.read<DocumentsCubit>().resetFilter();
|
await context.read<DocumentsCubit>().resetFilter();
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
} else {
|
} else {
|
||||||
if (filterIntent.filter !=
|
|
||||||
context.read<DocumentsCubit>().state.filter) {
|
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
}
|
|
||||||
await context
|
await context
|
||||||
.read<DocumentsCubit>()
|
.read<DocumentsCubit>()
|
||||||
.updateFilter(filter: filterIntent.filter!);
|
.updateFilter(filter: filterIntent.filter!);
|
||||||
@@ -339,73 +468,12 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDocumentCount(int count) {
|
void _openDetails(DocumentModel document) {
|
||||||
return count > 99 ? "99+" : count.toString();
|
Navigator.pushNamed(
|
||||||
}
|
context,
|
||||||
|
DocumentDetailsRoute.routeName,
|
||||||
Widget _buildBody(ConnectivityState connectivityState) {
|
arguments: DocumentDetailsRouteArguments(
|
||||||
final isConnected = connectivityState == ConnectivityState.connected;
|
document: document,
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
|
||||||
builder: (context, settings) {
|
|
||||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
!const ListEquality()
|
|
||||||
.equals(previous.documents, current.documents) ||
|
|
||||||
previous.selectedIds != current.selectedIds,
|
|
||||||
builder: (context, state) {
|
|
||||||
// Some ugly tricks to make it work with bloc, update pageController
|
|
||||||
|
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
|
||||||
return DocumentsEmptyState(
|
|
||||||
state: state,
|
|
||||||
onReset: () {
|
|
||||||
context.read<DocumentsCubit>().resetFilter();
|
|
||||||
context.read<DocumentsCubit>().unselectView();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AdaptiveDocumentsView(
|
|
||||||
viewType: settings.preferredViewType,
|
|
||||||
state: state,
|
|
||||||
scrollController: _scrollController,
|
|
||||||
onTap: _openDetails,
|
|
||||||
onSelected: _onSelected,
|
|
||||||
hasInternetConnection: isConnected,
|
|
||||||
onTagSelected: _addTagToFilter,
|
|
||||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
|
||||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
|
||||||
onStoragePathSelected: _addStoragePathToFilter,
|
|
||||||
pageLoadingWidget: const NewItemsLoadingWidget(),
|
|
||||||
beforeItems: const SizedBox(height: _savedViewWidgetHeight),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openDetails(DocumentModel document) async {
|
|
||||||
final potentiallyUpdatedModel =
|
|
||||||
await Navigator.of(context).push<DocumentModel?>(
|
|
||||||
_buildDetailsPageRoute(document),
|
|
||||||
);
|
|
||||||
if (potentiallyUpdatedModel != document) {
|
|
||||||
context.read<DocumentsCubit>().reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
|
|
||||||
DocumentModel document) {
|
|
||||||
return MaterialPageRoute(
|
|
||||||
builder: (_) => BlocProvider(
|
|
||||||
create: (context) => DocumentDetailsCubit(
|
|
||||||
context.read<PaperlessDocumentsApi>(),
|
|
||||||
document,
|
|
||||||
),
|
|
||||||
child: const LabelRepositoriesProvider(
|
|
||||||
child: DocumentDetailsPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -491,23 +559,19 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNewPage() async {
|
Future<void> _onReloadDocuments() async {
|
||||||
try {
|
try {
|
||||||
await context.read<DocumentsCubit>().loadMore();
|
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||||
|
await context.read<DocumentsCubit>().reload();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelected(DocumentModel model) {
|
Future<void> _onReloadSavedViews() async {
|
||||||
context.read<DocumentsCubit>().toggleDocumentSelection(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onRefresh() async {
|
|
||||||
try {
|
try {
|
||||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||||
context.read<DocumentsCubit>().reload();
|
await context.read<SavedViewCubit>().reload();
|
||||||
context.read<SavedViewCubit>().reload();
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|||||||
229
lib/features/documents/view/widgets/adaptive_documents_view.dart
Normal file
229
lib/features/documents/view/widgets/adaptive_documents_view.dart
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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/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';
|
||||||
|
|
||||||
|
abstract class AdaptiveDocumentsView extends StatelessWidget {
|
||||||
|
final List<DocumentModel> documents;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool hasLoaded;
|
||||||
|
final bool enableHeroAnimation;
|
||||||
|
final List<int> selectedDocumentIds;
|
||||||
|
final ViewType viewType;
|
||||||
|
final void Function(DocumentModel)? onTap;
|
||||||
|
final void Function(DocumentModel)? onSelected;
|
||||||
|
final bool hasInternetConnection;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
final void Function(int id)? onTagSelected;
|
||||||
|
final void Function(int? id)? onCorrespondentSelected;
|
||||||
|
final void Function(int? id)? onDocumentTypeSelected;
|
||||||
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
|
bool get showLoadingPlaceholder => (!hasLoaded && isLoading);
|
||||||
|
const AdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
this.selectedDocumentIds = const [],
|
||||||
|
required this.documents,
|
||||||
|
this.onTap,
|
||||||
|
this.onSelected,
|
||||||
|
this.viewType = ViewType.list,
|
||||||
|
required this.hasInternetConnection,
|
||||||
|
required this.isLabelClickable,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
required this.isLoading,
|
||||||
|
required this.hasLoaded,
|
||||||
|
this.enableHeroAnimation = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||||
|
const SliverAdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
required super.documents,
|
||||||
|
required super.hasInternetConnection,
|
||||||
|
required super.isLabelClickable,
|
||||||
|
super.onCorrespondentSelected,
|
||||||
|
super.onDocumentTypeSelected,
|
||||||
|
super.onStoragePathSelected,
|
||||||
|
super.onSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
super.selectedDocumentIds,
|
||||||
|
super.viewType,
|
||||||
|
super.enableHeroAnimation,
|
||||||
|
required super.isLoading,
|
||||||
|
required super.hasLoaded,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (viewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return _buildGridView();
|
||||||
|
case ViewType.list:
|
||||||
|
return _buildListView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView() {
|
||||||
|
if (showLoadingPlaceholder) {
|
||||||
|
return DocumentsListLoadingWidget.sliver();
|
||||||
|
}
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
childCount: documents.length,
|
||||||
|
(context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return LabelRepositoriesProvider(
|
||||||
|
child: DocumentListItem(
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridView() {
|
||||||
|
if (showLoadingPlaceholder) {
|
||||||
|
return DocumentGridLoadingWidget.sliver();
|
||||||
|
}
|
||||||
|
return SliverGrid.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 1 / 2,
|
||||||
|
),
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return DocumentGridItem(
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
const DefaultAdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
required super.documents,
|
||||||
|
required super.hasInternetConnection,
|
||||||
|
required super.isLabelClickable,
|
||||||
|
required super.isLoading,
|
||||||
|
required super.hasLoaded,
|
||||||
|
super.onCorrespondentSelected,
|
||||||
|
super.onDocumentTypeSelected,
|
||||||
|
super.onStoragePathSelected,
|
||||||
|
super.onSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
this.scrollController,
|
||||||
|
super.selectedDocumentIds,
|
||||||
|
super.viewType,
|
||||||
|
super.enableHeroAnimation = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
switch (viewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return _buildGridView();
|
||||||
|
case ViewType.list:
|
||||||
|
return _buildListView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView() {
|
||||||
|
if (showLoadingPlaceholder) {
|
||||||
|
return DocumentsListLoadingWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
primary: false,
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return LabelRepositoriesProvider(
|
||||||
|
child: DocumentListItem(
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridView() {
|
||||||
|
if (showLoadingPlaceholder) {
|
||||||
|
return DocumentGridLoadingWidget();
|
||||||
|
}
|
||||||
|
return GridView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
primary: false,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 1 / 2,
|
||||||
|
),
|
||||||
|
itemCount: documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final document = documents.elementAt(index);
|
||||||
|
return DocumentGridItem(
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: selectedDocumentIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isSelectionActive: selectedDocumentIds.isNotEmpty,
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
enableHeroAnimation: enableHeroAnimation,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,16 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
import 'package:paperless_mobile/core/widgets/empty_state.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/paged_document_view/model/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
class DocumentsEmptyState extends StatelessWidget {
|
class DocumentsEmptyState extends StatelessWidget {
|
||||||
final DocumentsState state;
|
final PagedDocumentsState state;
|
||||||
final VoidCallback onReset;
|
final VoidCallback? onReset;
|
||||||
const DocumentsEmptyState({
|
const DocumentsEmptyState({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.onReset,
|
this.onReset,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,7 +20,7 @@ class DocumentsEmptyState extends StatelessWidget {
|
|||||||
child: EmptyState(
|
child: EmptyState(
|
||||||
title: S.of(context).documentsPageEmptyStateOopsText,
|
title: S.of(context).documentsPageEmptyStateOopsText,
|
||||||
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
|
||||||
bottomChild: state.filter != DocumentFilter.initial
|
bottomChild: state.filter != DocumentFilter.initial && onReset != null
|
||||||
? TextButton(
|
? TextButton(
|
||||||
onPressed: onReset,
|
onPressed: onReset,
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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: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
|
||||||
|
with DocumentItemPlaceholder {
|
||||||
|
final bool _isSliver;
|
||||||
|
DocumentsListLoadingWidget({super.key}) : _isSliver = false;
|
||||||
|
|
||||||
|
DocumentsListLoadingWidget.sliver({super.key}) : _isSliver = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Random random = Random(1209571050);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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) {
|
||||||
|
const fontSize = 14.0;
|
||||||
|
final values = nextValues;
|
||||||
|
return ShimmerPlaceholder(
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(8),
|
||||||
|
dense: true,
|
||||||
|
isThreeLine: true,
|
||||||
|
leading: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
height: double.infinity,
|
||||||
|
width: 35,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
TextPlaceholder(
|
||||||
|
length: values.correspondentLength,
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
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!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DocumentGridItem extends StatelessWidget {
|
class DocumentGridItem extends DocumentItem {
|
||||||
final DocumentModel document;
|
|
||||||
final bool isSelected;
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
final bool isAtLeastOneSelected;
|
|
||||||
final bool Function(int tagId) isTagSelectedPredicate;
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
|
||||||
|
|
||||||
const DocumentGridItem({
|
const DocumentGridItem({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.document,
|
required super.document,
|
||||||
required this.onTap,
|
required super.isSelected,
|
||||||
required this.onSelected,
|
required super.isSelectionActive,
|
||||||
required this.isSelected,
|
required super.isLabelClickable,
|
||||||
required this.isAtLeastOneSelected,
|
super.onCorrespondentSelected,
|
||||||
required this.isTagSelectedPredicate,
|
super.onDocumentTypeSelected,
|
||||||
required this.onTagSelected,
|
super.onSelected,
|
||||||
}) : super(key: key);
|
super.onStoragePathSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
required super.enableHeroAnimation,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _onTap,
|
onTap: _onTap,
|
||||||
onLongPress: () => onSelected(document),
|
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||||
child: AbsorbPointer(
|
child: AbsorbPointer(
|
||||||
absorbing: isAtLeastOneSelected,
|
absorbing: isSelectionActive,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -48,6 +45,7 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
id: document.id,
|
id: document.id,
|
||||||
borderRadius: 12.0,
|
borderRadius: 12.0,
|
||||||
|
enableHero: enableHeroAnimation,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -94,10 +92,10 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onTap() {
|
void _onTap() {
|
||||||
if (isAtLeastOneSelected || isSelected) {
|
if (isSelectionActive || isSelected) {
|
||||||
onSelected(document);
|
onSelected?.call(document);
|
||||||
} else {
|
} else {
|
||||||
onTap(document);
|
onTap?.call(document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
32
lib/features/documents/view/widgets/items/document_item.dart
Normal file
32
lib/features/documents/view/widgets/items/document_item.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|
||||||
|
abstract class DocumentItem extends StatelessWidget {
|
||||||
|
final DocumentModel document;
|
||||||
|
final void Function(DocumentModel)? onTap;
|
||||||
|
final void Function(DocumentModel)? onSelected;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isSelectionActive;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
final bool enableHeroAnimation;
|
||||||
|
|
||||||
|
final void Function(int tagId)? onTagSelected;
|
||||||
|
final void Function(int? correspondentId)? onCorrespondentSelected;
|
||||||
|
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
||||||
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
|
||||||
|
const DocumentItem({
|
||||||
|
super.key,
|
||||||
|
required this.document,
|
||||||
|
this.onTap,
|
||||||
|
this.onSelected,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.isSelectionActive,
|
||||||
|
required this.isLabelClickable,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
required this.enableHeroAnimation,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
|
class DocumentListItem extends DocumentItem {
|
||||||
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
|
|
||||||
|
const DocumentListItem({
|
||||||
|
super.key,
|
||||||
|
required super.document,
|
||||||
|
required super.isSelected,
|
||||||
|
required super.isSelectionActive,
|
||||||
|
required super.isLabelClickable,
|
||||||
|
super.onCorrespondentSelected,
|
||||||
|
super.onDocumentTypeSelected,
|
||||||
|
super.onSelected,
|
||||||
|
super.onStoragePathSelected,
|
||||||
|
super.onTagSelected,
|
||||||
|
super.onTap,
|
||||||
|
super.enableHeroAnimation = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DocumentTypeBlocProvider(
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
selected: isSelected,
|
||||||
|
onTap: () => _onTap(),
|
||||||
|
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
onLongPress: () => onSelected?.call(document),
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
AbsorbPointer(
|
||||||
|
absorbing: isSelectionActive,
|
||||||
|
child: CorrespondentWidget(
|
||||||
|
isClickable: isLabelClickable,
|
||||||
|
correspondentId: document.correspondent,
|
||||||
|
onSelected: onCorrespondentSelected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
document.title,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
AbsorbPointer(
|
||||||
|
absorbing: isSelectionActive,
|
||||||
|
child: TagsWidget(
|
||||||
|
isClickable: isLabelClickable,
|
||||||
|
tagIds: document.tags,
|
||||||
|
isMultiLine: false,
|
||||||
|
onTagSelected: (id) => onTagSelected?.call(id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child:
|
||||||
|
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
||||||
|
builder: (context, docTypes) {
|
||||||
|
return RichText(
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
text: TextSpan(
|
||||||
|
text: DateFormat.yMMMd().format(document.created),
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.apply(color: Colors.grey),
|
||||||
|
children: document.documentType != null
|
||||||
|
? [
|
||||||
|
const TextSpan(text: '\u30FB'),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
docTypes.labels[document.documentType]?.name,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// Row(
|
||||||
|
// children: [
|
||||||
|
// Text(
|
||||||
|
// DateFormat.yMMMd().format(document.created),
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .textTheme
|
||||||
|
// .bodySmall
|
||||||
|
// ?.apply(color: Colors.grey),
|
||||||
|
// ),
|
||||||
|
// if (document.documentType != null) ...[
|
||||||
|
// Text("\u30FB"),
|
||||||
|
// DocumentTypeWidget(
|
||||||
|
// documentTypeId: document.documentType,
|
||||||
|
// textStyle: Theme.of(context).textTheme.bodySmall?.apply(
|
||||||
|
// color: Colors.grey,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
isThreeLine: document.tags.isNotEmpty,
|
||||||
|
leading: AspectRatio(
|
||||||
|
aspectRatio: _a4AspectRatio,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: DocumentPreview(
|
||||||
|
id: document.id,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
enableHero: enableHeroAnimation,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.all(8.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap() {
|
||||||
|
if (isSelectionActive || isSelected) {
|
||||||
|
onSelected?.call(document);
|
||||||
|
} else {
|
||||||
|
onTap?.call(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
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/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
|
|
||||||
class AdaptiveDocumentsView extends StatelessWidget {
|
|
||||||
final ViewType viewType;
|
|
||||||
final Widget beforeItems;
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
final ScrollController scrollController;
|
|
||||||
final DocumentsState state;
|
|
||||||
final bool hasInternetConnection;
|
|
||||||
final bool isLabelClickable;
|
|
||||||
final void Function(int id)? onTagSelected;
|
|
||||||
final void Function(int? id)? onCorrespondentSelected;
|
|
||||||
final void Function(int? id)? onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
final Widget pageLoadingWidget;
|
|
||||||
|
|
||||||
const AdaptiveDocumentsView({
|
|
||||||
super.key,
|
|
||||||
required this.onTap,
|
|
||||||
required this.scrollController,
|
|
||||||
required this.state,
|
|
||||||
required this.onSelected,
|
|
||||||
required this.hasInternetConnection,
|
|
||||||
this.isLabelClickable = true,
|
|
||||||
this.onTagSelected,
|
|
||||||
this.onCorrespondentSelected,
|
|
||||||
this.onDocumentTypeSelected,
|
|
||||||
this.onStoragePathSelected,
|
|
||||||
required this.pageLoadingWidget,
|
|
||||||
required this.beforeItems,
|
|
||||||
required this.viewType,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(child: beforeItems),
|
|
||||||
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SliverList _buildListView() {
|
|
||||||
return SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
childCount: state.documents.length,
|
|
||||||
(context, index) {
|
|
||||||
final document = state.documents.elementAt(index);
|
|
||||||
return LabelRepositoriesProvider(
|
|
||||||
child: DocumentListItem(
|
|
||||||
isLabelClickable: isLabelClickable,
|
|
||||||
document: document,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selectedIds.contains(document.id),
|
|
||||||
onSelected: onSelected,
|
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
|
||||||
isTagSelectedPredicate: (int tagId) {
|
|
||||||
return state.filter.tags is IdsTagsQuery
|
|
||||||
? (state.filter.tags as IdsTagsQuery)
|
|
||||||
.includedIds
|
|
||||||
.contains(tagId)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildGridView() {
|
|
||||||
return SliverGrid.builder(
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 4,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
childAspectRatio: 1 / 2,
|
|
||||||
),
|
|
||||||
itemCount: state.documents.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (state.hasLoaded &&
|
|
||||||
state.isLoading &&
|
|
||||||
index == state.documents.length) {
|
|
||||||
return Center(child: pageLoadingWidget);
|
|
||||||
}
|
|
||||||
final document = state.documents.elementAt(index);
|
|
||||||
return DocumentGridItem(
|
|
||||||
document: document,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selectedIds.contains(document.id),
|
|
||||||
onSelected: onSelected,
|
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
|
||||||
isTagSelectedPredicate: (int tagId) {
|
|
||||||
return state.filter.tags is IdsTagsQuery
|
|
||||||
? (state.filter.tags as IdsTagsQuery)
|
|
||||||
.includedIds
|
|
||||||
.contains(tagId)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
|
||||||
|
|
||||||
class DocumentListItem extends StatelessWidget {
|
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
|
||||||
final DocumentModel document;
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel)? onSelected;
|
|
||||||
final bool isSelected;
|
|
||||||
final bool isAtLeastOneSelected;
|
|
||||||
final bool isLabelClickable;
|
|
||||||
final bool Function(int tagId) isTagSelectedPredicate;
|
|
||||||
|
|
||||||
final void Function(int tagId)? onTagSelected;
|
|
||||||
final void Function(int? correspondentId)? onCorrespondentSelected;
|
|
||||||
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
|
|
||||||
const DocumentListItem({
|
|
||||||
Key? key,
|
|
||||||
required this.document,
|
|
||||||
required this.onTap,
|
|
||||||
this.onSelected,
|
|
||||||
required this.isSelected,
|
|
||||||
required this.isAtLeastOneSelected,
|
|
||||||
this.isLabelClickable = true,
|
|
||||||
required this.isTagSelectedPredicate,
|
|
||||||
this.onTagSelected,
|
|
||||||
this.onCorrespondentSelected,
|
|
||||||
this.onDocumentTypeSelected,
|
|
||||||
this.onStoragePathSelected,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
selected: isSelected,
|
|
||||||
onTap: () => _onTap(),
|
|
||||||
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
|
|
||||||
onLongPress: () => onSelected?.call(document),
|
|
||||||
title: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
AbsorbPointer(
|
|
||||||
absorbing: isAtLeastOneSelected,
|
|
||||||
child: CorrespondentWidget(
|
|
||||||
isClickable: isLabelClickable,
|
|
||||||
correspondentId: document.correspondent,
|
|
||||||
onSelected: onCorrespondentSelected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
document.title,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: document.tags.isEmpty ? 2 : 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: AbsorbPointer(
|
|
||||||
absorbing: isAtLeastOneSelected,
|
|
||||||
child: TagsWidget(
|
|
||||||
isClickable: isLabelClickable,
|
|
||||||
tagIds: document.tags,
|
|
||||||
isMultiLine: false,
|
|
||||||
onTagSelected: (id) => onTagSelected?.call(id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
isThreeLine: document.tags.isNotEmpty,
|
|
||||||
leading: AspectRatio(
|
|
||||||
aspectRatio: _a4AspectRatio,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: DocumentPreview(
|
|
||||||
id: document.id,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.all(8.0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTap() {
|
|
||||||
if (isAtLeastOneSelected || isSelected) {
|
|
||||||
onSelected?.call(document);
|
|
||||||
} else {
|
|
||||||
onTap(document);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class NewItemsLoadingWidget extends StatelessWidget {
|
class NewItemsLoadingWidget extends StatelessWidget {
|
||||||
const NewItemsLoadingWidget({super.key});
|
const NewItemsLoadingWidget({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const CircularProgressIndicator();
|
return Center(child: const CircularProgressIndicator().padded());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class OrderByDropdown extends StatefulWidget {
|
|
||||||
static const fkOrderBy = "orderBy";
|
|
||||||
const OrderByDropdown({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<OrderByDropdown> createState() => _OrderByDropdownState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OrderByDropdownState extends State<OrderByDropdown> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FormBuilderDropdown<SortField>(
|
|
||||||
name: OrderByDropdown.fkOrderBy,
|
|
||||||
items: const [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
mixin DocumentItemPlaceholder {
|
||||||
|
static const _tags = [" ", " ", " "];
|
||||||
|
static const _titleLengths = <double>[double.infinity, 150.0, 200.0];
|
||||||
|
static const _correspondentLengths = <double>[120.0, 80.0, 40.0];
|
||||||
|
|
||||||
|
Random get random;
|
||||||
|
|
||||||
|
RandomDocumentItemPlaceholderValues get nextValues {
|
||||||
|
return RandomDocumentItemPlaceholderValues(
|
||||||
|
tagCount: random.nextInt(_tags.length + 1),
|
||||||
|
correspondentLength: _correspondentLengths[
|
||||||
|
random.nextInt(_correspondentLengths.length - 1)],
|
||||||
|
titleLength: _titleLengths[random.nextInt(_titleLengths.length - 1)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RandomDocumentItemPlaceholderValues {
|
||||||
|
final int tagCount;
|
||||||
|
final double correspondentLength;
|
||||||
|
final double titleLength;
|
||||||
|
|
||||||
|
RandomDocumentItemPlaceholderValues({
|
||||||
|
required this.tagCount,
|
||||||
|
required this.correspondentLength,
|
||||||
|
required this.titleLength,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TagsPlaceholder extends StatelessWidget {
|
||||||
|
static const _lengths = [24, 36, 16, 48];
|
||||||
|
final int count;
|
||||||
|
final bool dense;
|
||||||
|
const TagsPlaceholder({
|
||||||
|
super.key,
|
||||||
|
required this.count,
|
||||||
|
required this.dense,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: ListView.separated(
|
||||||
|
itemCount: count,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemBuilder: (context, index) => FilterChip(
|
||||||
|
labelPadding:
|
||||||
|
dense ? const EdgeInsets.symmetric(horizontal: 2) : null,
|
||||||
|
padding: dense ? const EdgeInsets.all(4) : null,
|
||||||
|
visualDensity: const VisualDensity(vertical: -2),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
side: BorderSide.none,
|
||||||
|
onSelected: (_) {},
|
||||||
|
selected: false,
|
||||||
|
label: Text(
|
||||||
|
List.filled(_lengths[index], " ").join(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
separatorBuilder: (context, _) => const SizedBox(width: 4),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
|
class TextPlaceholder extends StatelessWidget {
|
||||||
|
final double length;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const TextPlaceholder({
|
||||||
|
super.key,
|
||||||
|
required this.length,
|
||||||
|
required this.fontSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
width: length,
|
||||||
|
height: fontSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.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/tags/view/widgets/tags_form_field.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
|
import 'text_query_form_field.dart';
|
||||||
|
|
||||||
|
class DocumentFilterForm extends StatefulWidget {
|
||||||
|
static const fkCorrespondent = DocumentModel.correspondentKey;
|
||||||
|
static const fkDocumentType = DocumentModel.documentTypeKey;
|
||||||
|
static const fkStoragePath = DocumentModel.storagePathKey;
|
||||||
|
static const fkQuery = "query";
|
||||||
|
static const fkCreatedAt = DocumentModel.createdKey;
|
||||||
|
static const fkAddedAt = DocumentModel.addedKey;
|
||||||
|
|
||||||
|
static DocumentFilter assembleFilter(
|
||||||
|
GlobalKey<FormBuilderState> formKey, DocumentFilter initialFilter) {
|
||||||
|
formKey.currentState?.save();
|
||||||
|
final v = formKey.currentState!.value;
|
||||||
|
return DocumentFilter(
|
||||||
|
correspondent:
|
||||||
|
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.correspondent,
|
||||||
|
documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.documentType,
|
||||||
|
storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ??
|
||||||
|
DocumentFilter.initial.storagePath,
|
||||||
|
tags:
|
||||||
|
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||||
|
query: v[DocumentFilterForm.fkQuery] as TextQuery? ??
|
||||||
|
DocumentFilter.initial.query,
|
||||||
|
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
|
||||||
|
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
|
||||||
|
asnQuery: initialFilter.asnQuery,
|
||||||
|
page: 1,
|
||||||
|
pageSize: initialFilter.pageSize,
|
||||||
|
sortField: initialFilter.sortField,
|
||||||
|
sortOrder: initialFilter.sortOrder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget? header;
|
||||||
|
final GlobalKey<FormBuilderState> formKey;
|
||||||
|
final DocumentFilter initialFilter;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
const DocumentFilterForm({
|
||||||
|
super.key,
|
||||||
|
this.header,
|
||||||
|
required this.formKey,
|
||||||
|
required this.initialFilter,
|
||||||
|
this.scrollController,
|
||||||
|
this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentFilterForm> createState() => _DocumentFilterFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||||
|
late bool _allowOnlyExtendedQuery;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FormBuilder(
|
||||||
|
key: widget.formKey,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
slivers: [
|
||||||
|
if (widget.header != null) widget.header!,
|
||||||
|
..._buildFormFieldList(),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildFormFieldList() {
|
||||||
|
return [
|
||||||
|
_buildQueryFormField(),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
S.of(context).documentFilterAdvancedLabel,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FormBuilderExtendedDateRangePicker(
|
||||||
|
name: DocumentFilterForm.fkCreatedAt,
|
||||||
|
initialValue: widget.initialFilter.created,
|
||||||
|
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
FormBuilderExtendedDateRangePicker(
|
||||||
|
name: DocumentFilterForm.fkAddedAt,
|
||||||
|
initialValue: widget.initialFilter.added,
|
||||||
|
labelText: S.of(context).documentAddedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildCorrespondentFormField(),
|
||||||
|
_buildDocumentTypeFormField(),
|
||||||
|
_buildStoragePathFormField(),
|
||||||
|
_buildTagsFormField(),
|
||||||
|
]
|
||||||
|
.map((w) => SliverPadding(
|
||||||
|
padding: widget.padding,
|
||||||
|
sliver: SliverToBoxAdapter(child: w),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkQueryConstraints() {
|
||||||
|
final filter =
|
||||||
|
DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
|
||||||
|
if (filter.forceExtendedQuery) {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = true);
|
||||||
|
final queryField =
|
||||||
|
widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
|
||||||
|
queryField?.didChange(
|
||||||
|
(queryField.value as TextQuery?)
|
||||||
|
?.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDocumentTypeFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<DocumentType>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkDocumentType,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.documentType,
|
||||||
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCorrespondentFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<Correspondent>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkCorrespondent,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.correspondent,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStoragePathFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return LabelFormField<StoragePath>(
|
||||||
|
formBuilderState: widget.formKey.currentState,
|
||||||
|
name: DocumentFilterForm.fkStoragePath,
|
||||||
|
labelOptions: state.labels,
|
||||||
|
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||||
|
initialValue: widget.initialFilter.storagePath,
|
||||||
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQueryFormField() {
|
||||||
|
return TextQueryFormField(
|
||||||
|
name: DocumentFilterForm.fkQuery,
|
||||||
|
onlyExtendedQueryAllowed: _allowOnlyExtendedQuery,
|
||||||
|
initialValue: widget.initialFilter.query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
||||||
|
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return TagFormField(
|
||||||
|
name: DocumentModel.tagsKey,
|
||||||
|
initialValue: widget.initialFilter.tags,
|
||||||
|
allowCreation: false,
|
||||||
|
selectableOptions: state.labels,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
@@ -32,22 +33,14 @@ class DocumentFilterPanel extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||||
static const fkCorrespondent = DocumentModel.correspondentKey;
|
|
||||||
static const fkDocumentType = DocumentModel.documentTypeKey;
|
|
||||||
static const fkStoragePath = DocumentModel.storagePathKey;
|
|
||||||
static const fkQuery = "query";
|
|
||||||
static const fkCreatedAt = DocumentModel.createdKey;
|
|
||||||
static const fkAddedAt = DocumentModel.addedKey;
|
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
late bool _allowOnlyExtendedQuery;
|
|
||||||
|
|
||||||
double _heightAnimationValue = 0;
|
double _heightAnimationValue = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
|
||||||
widget.draggableSheetController.addListener(animateTitleByDrag);
|
widget.draggableSheetController.addListener(animateTitleByDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,100 +99,59 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: FormBuilder(
|
body: DocumentFilterForm(
|
||||||
key: _formKey,
|
formKey: _formKey,
|
||||||
child: _buildFormList(context),
|
scrollController: widget.scrollController,
|
||||||
|
initialFilter: widget.initialFilter,
|
||||||
|
header: _buildPanelHeader(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormList(BuildContext context) {
|
Widget _buildPanelHeader() {
|
||||||
return CustomScrollView(
|
return SliverAppBar(
|
||||||
controller: widget.scrollController,
|
pinned: true,
|
||||||
slivers: [
|
automaticallyImplyLeading: false,
|
||||||
SliverAppBar(
|
toolbarHeight: kToolbarHeight + 22,
|
||||||
pinned: true,
|
title: SizedBox(
|
||||||
automaticallyImplyLeading: false,
|
width: MediaQuery.of(context).size.width,
|
||||||
toolbarHeight: kToolbarHeight + 22,
|
child: Column(
|
||||||
title: SizedBox(
|
mainAxisSize: MainAxisSize.max,
|
||||||
width: MediaQuery.of(context).size.width,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.max,
|
Opacity(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
opacity: 1 - _heightAnimationValue,
|
||||||
children: [
|
child: Padding(
|
||||||
Opacity(
|
padding: const EdgeInsets.only(bottom: 11),
|
||||||
opacity: 1 - _heightAnimationValue,
|
child: _buildDragHandle(),
|
||||||
child: Padding(
|
),
|
||||||
padding: EdgeInsets.only(bottom: 11),
|
|
||||||
child: _buildDragHandle(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
children: [
|
|
||||||
Opacity(
|
|
||||||
opacity: max(0, (_heightAnimationValue - 0.5) * 2),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Navigator.of(context).pop(),
|
|
||||||
child: const Icon(Icons.expand_more_rounded),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(left: _heightAnimationValue * 48),
|
|
||||||
child: Text(S.of(context).documentFilterTitle),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: max(0, (_heightAnimationValue - 0.5) * 2),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
child: const Icon(Icons.expand_more_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: _heightAnimationValue * 48),
|
||||||
|
child: Text(S.of(context).documentFilterTitle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
..._buildFormFieldList(),
|
),
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildFormFieldList() {
|
|
||||||
return [
|
|
||||||
_buildQueryFormField().paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
S.of(context).documentFilterAdvancedLabel,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
FormBuilderExtendedDateRangePicker(
|
|
||||||
name: fkCreatedAt,
|
|
||||||
initialValue: widget.initialFilter.created,
|
|
||||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
|
||||||
onChanged: (_) {
|
|
||||||
_checkQueryConstraints();
|
|
||||||
},
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
FormBuilderExtendedDateRangePicker(
|
|
||||||
name: fkAddedAt,
|
|
||||||
initialValue: widget.initialFilter.added,
|
|
||||||
labelText: S.of(context).documentAddedPropertyLabel,
|
|
||||||
onChanged: (_) {
|
|
||||||
_checkQueryConstraints();
|
|
||||||
},
|
|
||||||
).paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildCorrespondentFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildDocumentTypeFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildStoragePathFormField()
|
|
||||||
.paddedSymmetrically(vertical: 8, horizontal: 16),
|
|
||||||
_buildTagsFormField().padded(16),
|
|
||||||
].map((w) => SliverToBoxAdapter(child: w)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Container _buildDragHandle() {
|
Container _buildDragHandle() {
|
||||||
return Container(
|
return Container(
|
||||||
// According to m3 spec https://m3.material.io/components/bottom-sheets/specs
|
// According to m3 spec https://m3.material.io/components/bottom-sheets/specs
|
||||||
@@ -212,19 +164,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return TagFormField(
|
|
||||||
name: DocumentModel.tagsKey,
|
|
||||||
initialValue: widget.initialFilter.tags,
|
|
||||||
allowCreation: false,
|
|
||||||
selectableOptions: state.labels,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetFilter() async {
|
void _resetFilter() async {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
@@ -233,102 +172,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<DocumentType>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkDocumentType,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.documentType,
|
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCorrespondentFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<Correspondent>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkCorrespondent,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.correspondent,
|
|
||||||
prefixIcon: const Icon(Icons.person_outline),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStoragePathFormField() {
|
|
||||||
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return LabelFormField<StoragePath>(
|
|
||||||
formBuilderState: _formKey.currentState,
|
|
||||||
name: fkStoragePath,
|
|
||||||
labelOptions: state.labels,
|
|
||||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
|
||||||
initialValue: widget.initialFilter.storagePath,
|
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQueryFormField() {
|
|
||||||
return TextQueryFormField(
|
|
||||||
name: fkQuery,
|
|
||||||
onlyExtendedQueryAllowed: _allowOnlyExtendedQuery,
|
|
||||||
initialValue: widget.initialFilter.query,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onApplyFilter() async {
|
void _onApplyFilter() async {
|
||||||
_formKey.currentState?.save();
|
_formKey.currentState?.save();
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
DocumentFilter newFilter = _assembleFilter();
|
DocumentFilter newFilter =
|
||||||
|
DocumentFilterForm.assembleFilter(_formKey, widget.initialFilter);
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(context, DocumentFilterIntent(filter: newFilter));
|
Navigator.pop(context, DocumentFilterIntent(filter: newFilter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentFilter _assembleFilter() {
|
|
||||||
_formKey.currentState?.save();
|
|
||||||
final v = _formKey.currentState!.value;
|
|
||||||
return DocumentFilter(
|
|
||||||
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.correspondent,
|
|
||||||
documentType: v[fkDocumentType] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.documentType,
|
|
||||||
storagePath: v[fkStoragePath] as IdQueryParameter? ??
|
|
||||||
DocumentFilter.initial.storagePath,
|
|
||||||
tags:
|
|
||||||
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
|
||||||
query: v[fkQuery] as TextQuery? ?? DocumentFilter.initial.query,
|
|
||||||
created: (v[fkCreatedAt] as DateRangeQuery),
|
|
||||||
added: (v[fkAddedAt] as DateRangeQuery),
|
|
||||||
asnQuery: widget.initialFilter.asnQuery,
|
|
||||||
page: 1,
|
|
||||||
pageSize: widget.initialFilter.pageSize,
|
|
||||||
sortField: widget.initialFilter.sortField,
|
|
||||||
sortOrder: widget.initialFilter.sortOrder,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _checkQueryConstraints() {
|
|
||||||
final filter = _assembleFilter();
|
|
||||||
if (filter.forceExtendedQuery) {
|
|
||||||
setState(() => _allowOnlyExtendedQuery = true);
|
|
||||||
final queryField = _formKey.currentState?.fields[fkQuery];
|
|
||||||
queryField?.didChange(
|
|
||||||
(queryField.value as TextQuery?)
|
|
||||||
?.copyWith(queryType: QueryType.extended),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setState(() => _allowOnlyExtendedQuery = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
@@ -8,9 +10,9 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
|
|
||||||
class SortFieldSelectionBottomSheet extends StatefulWidget {
|
class SortFieldSelectionBottomSheet extends StatefulWidget {
|
||||||
final SortOrder initialSortOrder;
|
final SortOrder initialSortOrder;
|
||||||
final SortField initialSortField;
|
final SortField? initialSortField;
|
||||||
|
|
||||||
final Future Function(SortField field, SortOrder order) onSubmit;
|
final Future Function(SortField? field, SortOrder order) onSubmit;
|
||||||
|
|
||||||
const SortFieldSelectionBottomSheet({
|
const SortFieldSelectionBottomSheet({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -26,7 +28,7 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _SortFieldSelectionBottomSheetState
|
class _SortFieldSelectionBottomSheetState
|
||||||
extends State<SortFieldSelectionBottomSheet> {
|
extends State<SortFieldSelectionBottomSheet> {
|
||||||
late SortField _currentSortField;
|
late SortField? _currentSortField;
|
||||||
late SortOrder _currentSortOrder;
|
late SortOrder _currentSortOrder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,61 +41,90 @@ class _SortFieldSelectionBottomSheetState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text(
|
children: [
|
||||||
S.of(context).documentsPageOrderByLabel,
|
Text(
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
S.of(context).documentsPageOrderByLabel,
|
||||||
textAlign: TextAlign.start,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
textAlign: TextAlign.start,
|
||||||
TextButton(
|
),
|
||||||
child: Text(S.of(context).documentFilterApplyFilterLabel),
|
TextButton(
|
||||||
onPressed: () {
|
child: Text(S.of(context).documentFilterApplyFilterLabel),
|
||||||
widget.onSubmit(
|
onPressed: () {
|
||||||
_currentSortField,
|
widget.onSubmit(
|
||||||
_currentSortOrder,
|
_currentSortField,
|
||||||
);
|
_currentSortOrder,
|
||||||
Navigator.pop(context);
|
);
|
||||||
},
|
Navigator.pop(context);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
).paddedSymmetrically(horizontal: 16, vertical: 8.0),
|
],
|
||||||
Column(
|
).paddedOnly(left: 16, right: 16, top: 8),
|
||||||
children: [
|
Column(
|
||||||
_buildSortOption(SortField.archiveSerialNumber),
|
children: [
|
||||||
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
|
_buildSortOption(SortField.archiveSerialNumber),
|
||||||
builder: (context, state) {
|
BlocBuilder<LabelCubit<Correspondent>,
|
||||||
return _buildSortOption(
|
LabelState<Correspondent>>(
|
||||||
SortField.correspondentName,
|
builder: (context, state) {
|
||||||
enabled: state.labels.values.fold<bool>(
|
return _buildSortOption(
|
||||||
false,
|
SortField.correspondentName,
|
||||||
(previousValue, element) =>
|
enabled: state.labels.values.fold<bool>(
|
||||||
previousValue || (element.documentCount ?? 0) > 0),
|
false,
|
||||||
);
|
(previousValue, element) =>
|
||||||
},
|
previousValue ||
|
||||||
),
|
(element.documentCount ?? 0) > 0),
|
||||||
_buildSortOption(SortField.title),
|
);
|
||||||
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
},
|
||||||
builder: (context, state) {
|
),
|
||||||
return _buildSortOption(
|
_buildSortOption(SortField.title),
|
||||||
SortField.documentType,
|
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
|
||||||
enabled: state.labels.values.fold<bool>(
|
builder: (context, state) {
|
||||||
false,
|
return _buildSortOption(
|
||||||
(previousValue, element) =>
|
SortField.documentType,
|
||||||
previousValue || (element.documentCount ?? 0) > 0),
|
enabled: state.labels.values.fold<bool>(
|
||||||
);
|
false,
|
||||||
},
|
(previousValue, element) =>
|
||||||
),
|
previousValue ||
|
||||||
_buildSortOption(SortField.created),
|
(element.documentCount ?? 0) > 0),
|
||||||
_buildSortOption(SortField.added),
|
);
|
||||||
_buildSortOption(SortField.modified),
|
},
|
||||||
],
|
),
|
||||||
),
|
_buildSortOption(SortField.created),
|
||||||
],
|
_buildSortOption(SortField.added),
|
||||||
|
_buildSortOption(SortField.modified),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: SegmentedButton(
|
||||||
|
multiSelectionEnabled: false,
|
||||||
|
showSelectedIcon: false,
|
||||||
|
segments: [
|
||||||
|
ButtonSegment(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.arrowDownAZ),
|
||||||
|
value: SortOrder.descending,
|
||||||
|
label: Text(S.of(context).sortDocumentDescending),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
icon: const FaIcon(FontAwesomeIcons.arrowUpZA),
|
||||||
|
value: SortOrder.ascending,
|
||||||
|
label: Text(S.of(context).sortDocumentAscending),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
emptySelectionAllowed: false,
|
||||||
|
selected: {_currentSortOrder},
|
||||||
|
onSelectionChanged: (selection) {
|
||||||
|
setState(() => _currentSortOrder = selection.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).paddedOnly(bottom: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,47 +132,10 @@ class _SortFieldSelectionBottomSheetState
|
|||||||
Widget _buildSortOption(SortField field, {bool enabled = true}) {
|
Widget _buildSortOption(SortField field, {bool enabled = true}) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
|
contentPadding: const EdgeInsets.only(left: 32, right: 16),
|
||||||
title: Text(
|
title: Text(translateSortField(context, field)),
|
||||||
_localizedSortField(field),
|
trailing: _currentSortField == field ? const Icon(Icons.done) : null,
|
||||||
),
|
onTap: () => setState(() => _currentSortField = field),
|
||||||
trailing: _currentSortField == field
|
|
||||||
? _buildOrderIcon(_currentSortOrder)
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
_currentSortOrder = (_currentSortField == field
|
|
||||||
? _currentSortOrder.toggle()
|
|
||||||
: SortOrder.descending);
|
|
||||||
_currentSortField = field;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOrderIcon(SortOrder order) {
|
|
||||||
if (order == SortOrder.ascending) {
|
|
||||||
return const Icon(Icons.arrow_upward);
|
|
||||||
}
|
|
||||||
return const Icon(Icons.arrow_downward);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _localizedSortField(SortField sortField) {
|
|
||||||
switch (sortField) {
|
|
||||||
case SortField.archiveSerialNumber:
|
|
||||||
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
|
|
||||||
case SortField.correspondentName:
|
|
||||||
return S.of(context).documentCorrespondentPropertyLabel;
|
|
||||||
case SortField.title:
|
|
||||||
return S.of(context).documentTitlePropertyLabel;
|
|
||||||
case SortField.documentType:
|
|
||||||
return S.of(context).documentDocumentTypePropertyLabel;
|
|
||||||
case SortField.created:
|
|
||||||
return S.of(context).documentCreatedPropertyLabel;
|
|
||||||
case SortField.added:
|
|
||||||
return S.of(context).documentAddedPropertyLabel;
|
|
||||||
case SortField.modified:
|
|
||||||
return S.of(context).documentModifiedPropertyLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
|
||||||
import 'package:paperless_mobile/util.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
|
|
||||||
class DocumentsPageAppBar extends StatefulWidget with PreferredSizeWidget {
|
|
||||||
final List<Widget> actions;
|
|
||||||
final bool isOffline;
|
|
||||||
|
|
||||||
const DocumentsPageAppBar({
|
|
||||||
super.key,
|
|
||||||
required this.isOffline,
|
|
||||||
this.actions = const [],
|
|
||||||
});
|
|
||||||
@override
|
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
|
||||||
@override
|
|
||||||
State<DocumentsPageAppBar> createState() => _DocumentsPageAppBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const savedViewWidgetHeight = 48.0;
|
|
||||||
final flexibleAreaHeight = kToolbarHeight -
|
|
||||||
16 +
|
|
||||||
savedViewWidgetHeight +
|
|
||||||
(widget.isOffline ? 24 : 0);
|
|
||||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, documentsState) {
|
|
||||||
final hasSelection = documentsState.selection.isNotEmpty;
|
|
||||||
// final PreferredSize? loadingWidget = documentsState.isLoading
|
|
||||||
// ? const PreferredSize(
|
|
||||||
// child: LinearProgressIndicator(),
|
|
||||||
// preferredSize: Size.fromHeight(4.0),
|
|
||||||
// )
|
|
||||||
// : null;
|
|
||||||
if (hasSelection) {
|
|
||||||
return SliverAppBar(
|
|
||||||
// bottom: loadingWidget,
|
|
||||||
expandedHeight: kToolbarHeight + flexibleAreaHeight,
|
|
||||||
snap: true,
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
flexibleSpace: _buildFlexibleArea(
|
|
||||||
false,
|
|
||||||
documentsState.filter,
|
|
||||||
savedViewWidgetHeight,
|
|
||||||
),
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () => context.read<DocumentsCubit>().resetSelection(),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'${documentsState.selection.length} ${S.of(context).documentsSelectedText}'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () => _onDelete(context, documentsState),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SliverAppBar(
|
|
||||||
// bottom: loadingWidget,
|
|
||||||
expandedHeight: kToolbarHeight + flexibleAreaHeight,
|
|
||||||
snap: true,
|
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
|
||||||
flexibleSpace: _buildFlexibleArea(
|
|
||||||
true,
|
|
||||||
documentsState.filter,
|
|
||||||
savedViewWidgetHeight,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
...widget.actions,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFlexibleArea(
|
|
||||||
bool enabled,
|
|
||||||
DocumentFilter filter,
|
|
||||||
double savedViewHeight,
|
|
||||||
) {
|
|
||||||
return FlexibleSpaceBar(
|
|
||||||
background: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if (widget.isOffline) const OfflineBanner(),
|
|
||||||
SavedViewSelectionWidget(
|
|
||||||
height: savedViewHeight,
|
|
||||||
enabled: enabled,
|
|
||||||
currentFilter: filter,
|
|
||||||
).paddedSymmetrically(horizontal: 8.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
|
||||||
final shouldDelete = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
BulkDeleteConfirmationDialog(state: documentsState)) ??
|
|
||||||
false;
|
|
||||||
if (shouldDelete) {
|
|
||||||
try {
|
|
||||||
await context
|
|
||||||
.read<DocumentsCubit>()
|
|
||||||
.bulkRemove(documentsState.selection);
|
|
||||||
showSnackBar(
|
|
||||||
context,
|
|
||||||
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
|
||||||
);
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDocumentCount(int count) {
|
|
||||||
return count > 99 ? "99+" : count.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScrollListener extends ChangeNotifier {
|
|
||||||
double top = 0;
|
|
||||||
double _last = 0;
|
|
||||||
|
|
||||||
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
|
|
||||||
controller.addListener(() {
|
|
||||||
final current = controller.offset;
|
|
||||||
top += _last - current;
|
|
||||||
if (top <= -height) top = -height;
|
|
||||||
if (top >= 0) top = 0;
|
|
||||||
_last = current;
|
|
||||||
if (top <= 0 && top >= -height) notifyListeners();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,72 +4,72 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatelessWidget {
|
class SortDocumentsButton extends StatelessWidget {
|
||||||
const SortDocumentsButton({super.key});
|
const SortDocumentsButton({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
icon: const Icon(Icons.sort),
|
builder: (context, state) {
|
||||||
onPressed: () => _onOpenSortBottomSheet(context),
|
if (state.filter.sortField == null) {
|
||||||
);
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
return TextButton.icon(
|
||||||
void _onOpenSortBottomSheet(BuildContext context) {
|
icon: Icon(state.filter.sortOrder == SortOrder.ascending
|
||||||
showModalBottomSheet(
|
? Icons.arrow_upward
|
||||||
elevation: 2,
|
: Icons.arrow_downward),
|
||||||
context: context,
|
label: Text(translateSortField(context, state.filter.sortField)),
|
||||||
isScrollControlled: true,
|
onPressed: () {
|
||||||
shape: const RoundedRectangleBorder(
|
showModalBottomSheet(
|
||||||
borderRadius: BorderRadius.only(
|
elevation: 2,
|
||||||
topLeft: Radius.circular(16),
|
context: context,
|
||||||
topRight: Radius.circular(16),
|
isScrollControlled: true,
|
||||||
),
|
shape: const RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.only(
|
||||||
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
topLeft: Radius.circular(16),
|
||||||
value: context.read<DocumentsCubit>(),
|
topRight: Radius.circular(16),
|
||||||
child: FractionallySizedBox(
|
|
||||||
heightFactor: .6,
|
|
||||||
child: MultiBlocProvider(
|
|
||||||
providers: [
|
|
||||||
BlocProvider(
|
|
||||||
create: (context) => LabelCubit<DocumentType>(
|
|
||||||
context.read<
|
|
||||||
LabelRepository<DocumentType,
|
|
||||||
DocumentTypeRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
builder: (_) => BlocProvider<DocumentsCubit>.value(
|
||||||
create: (context) => LabelCubit<Correspondent>(
|
value: context.read<DocumentsCubit>(),
|
||||||
context.read<
|
child: MultiBlocProvider(
|
||||||
LabelRepository<Correspondent,
|
providers: [
|
||||||
CorrespondentRepositoryState>>(),
|
BlocProvider(
|
||||||
),
|
create: (context) => LabelCubit<DocumentType>(
|
||||||
),
|
context.read<LabelRepository<DocumentType>>(),
|
||||||
],
|
),
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
),
|
||||||
builder: (context, state) {
|
BlocProvider(
|
||||||
return SortFieldSelectionBottomSheet(
|
create: (context) => LabelCubit<Correspondent>(
|
||||||
initialSortField: state.filter.sortField,
|
context.read<LabelRepository<Correspondent>>(),
|
||||||
initialSortOrder: state.filter.sortOrder,
|
),
|
||||||
onSubmit: (field, order) =>
|
),
|
||||||
context.read<DocumentsCubit>().updateCurrentFilter(
|
],
|
||||||
(filter) => filter.copyWith(
|
child: SortFieldSelectionBottomSheet(
|
||||||
sortField: field,
|
initialSortField: state.filter.sortField,
|
||||||
sortOrder: order,
|
initialSortOrder: state.filter.sortOrder,
|
||||||
|
onSubmit: (field, order) =>
|
||||||
|
context.read<DocumentsCubit>().updateCurrentFilter(
|
||||||
|
(filter) => filter.copyWith(
|
||||||
|
sortField: field,
|
||||||
|
sortOrder: order,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
lib/features/documents/view/widgets/view_actions.dart
Normal file
39
lib/features/documents/view/widgets/view_actions.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
class ViewActions extends StatelessWidget {
|
||||||
|
const ViewActions({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const SortDocumentsButton(),
|
||||||
|
BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
|
builder: (context, settings) {
|
||||||
|
final cubit = context.read<ApplicationSettingsCubit>();
|
||||||
|
switch (settings.preferredViewType) {
|
||||||
|
case ViewType.grid:
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.list),
|
||||||
|
onPressed: () =>
|
||||||
|
cubit.setViewType(settings.preferredViewType.toggle()),
|
||||||
|
);
|
||||||
|
case ViewType.list:
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.grid_view_rounded),
|
||||||
|
onPressed: () =>
|
||||||
|
cubit.setViewType(settings.preferredViewType.toggle()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
||||||
@@ -16,31 +17,28 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
|||||||
final DocumentModel _initialDocument;
|
final DocumentModel _initialDocument;
|
||||||
final PaperlessDocumentsApi _docsApi;
|
final PaperlessDocumentsApi _docsApi;
|
||||||
|
|
||||||
final LabelRepository<Correspondent, CorrespondentRepositoryState>
|
final DocumentChangedNotifier _notifier;
|
||||||
_correspondentRepository;
|
final LabelRepository<Correspondent> _correspondentRepository;
|
||||||
final LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||||
_documentTypeRepository;
|
final LabelRepository<StoragePath> _storagePathRepository;
|
||||||
final LabelRepository<StoragePath, StoragePathRepositoryState>
|
final LabelRepository<Tag> _tagRepository;
|
||||||
_storagePathRepository;
|
|
||||||
final LabelRepository<Tag, TagRepositoryState> _tagRepository;
|
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
EditDocumentCubit(
|
EditDocumentCubit(
|
||||||
DocumentModel document, {
|
DocumentModel document, {
|
||||||
required PaperlessDocumentsApi documentsApi,
|
required PaperlessDocumentsApi documentsApi,
|
||||||
required LabelRepository<Correspondent, CorrespondentRepositoryState>
|
required LabelRepository<Correspondent> correspondentRepository,
|
||||||
correspondentRepository,
|
required LabelRepository<DocumentType> documentTypeRepository,
|
||||||
required LabelRepository<DocumentType, DocumentTypeRepositoryState>
|
required LabelRepository<StoragePath> storagePathRepository,
|
||||||
documentTypeRepository,
|
required LabelRepository<Tag> tagRepository,
|
||||||
required LabelRepository<StoragePath, StoragePathRepositoryState>
|
required DocumentChangedNotifier notifier,
|
||||||
storagePathRepository,
|
|
||||||
required LabelRepository<Tag, TagRepositoryState> tagRepository,
|
|
||||||
}) : _initialDocument = document,
|
}) : _initialDocument = document,
|
||||||
_docsApi = documentsApi,
|
_docsApi = documentsApi,
|
||||||
_correspondentRepository = correspondentRepository,
|
_correspondentRepository = correspondentRepository,
|
||||||
_documentTypeRepository = documentTypeRepository,
|
_documentTypeRepository = documentTypeRepository,
|
||||||
_storagePathRepository = storagePathRepository,
|
_storagePathRepository = storagePathRepository,
|
||||||
_tagRepository = tagRepository,
|
_tagRepository = tagRepository,
|
||||||
|
_notifier = notifier,
|
||||||
super(
|
super(
|
||||||
EditDocumentState(
|
EditDocumentState(
|
||||||
document: document,
|
document: document,
|
||||||
@@ -50,6 +48,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
|||||||
tags: tagRepository.current?.values ?? {},
|
tags: tagRepository.current?.values ?? {},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
_notifier.subscribe(this, onUpdated: replace);
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
_correspondentRepository.values
|
_correspondentRepository.values
|
||||||
.listen((v) => emit(state.copyWith(correspondents: v?.values))),
|
.listen((v) => emit(state.copyWith(correspondents: v?.values))),
|
||||||
@@ -71,6 +70,8 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
|||||||
|
|
||||||
Future<void> updateDocument(DocumentModel document) async {
|
Future<void> updateDocument(DocumentModel document) async {
|
||||||
final updated = await _docsApi.update(document);
|
final updated = await _docsApi.update(document);
|
||||||
|
_notifier.notifyUpdated(updated);
|
||||||
|
|
||||||
// Reload changed labels (documentCount property changes with removal/add)
|
// Reload changed labels (documentCount property changes with removal/add)
|
||||||
if (document.documentType != _initialDocument.documentType) {
|
if (document.documentType != _initialDocument.documentType) {
|
||||||
_documentTypeRepository
|
_documentTypeRepository
|
||||||
@@ -88,7 +89,10 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
|||||||
.equals(document.tags, _initialDocument.tags)) {
|
.equals(document.tags, _initialDocument.tags)) {
|
||||||
_tagRepository.findAll(document.tags);
|
_tagRepository.findAll(document.tags);
|
||||||
}
|
}
|
||||||
emit(state.copyWith(document: updated));
|
}
|
||||||
|
|
||||||
|
void replace(DocumentModel document) {
|
||||||
|
emit(state.copyWith(document: document));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -96,6 +100,7 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
|||||||
for (final sub in _subscriptions) {
|
for (final sub in _subscriptions) {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
}
|
}
|
||||||
|
_notifier.unsubscribe(this);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import 'dart:async';
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart';
|
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart';
|
||||||
|
|
||||||
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
|
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
|
||||||
final LabelRepository<T, RepositoryState<Map<int, T>>> _repository;
|
final LabelRepository<T> _repository;
|
||||||
|
|
||||||
StreamSubscription? _subscription;
|
StreamSubscription? _subscription;
|
||||||
|
|
||||||
EditLabelCubit(LabelRepository<T, RepositoryState<Map<int, T>>> repository)
|
EditLabelCubit(LabelRepository<T> repository)
|
||||||
: _repository = repository,
|
: _repository = repository,
|
||||||
super(const EditLabelInitial()) {
|
super(const EditLabelInitial()) {
|
||||||
_subscription = repository.values.listen(
|
_subscription = repository.values.listen(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
|
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
|
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
@@ -25,8 +25,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit(
|
create: (context) => EditLabelCubit(
|
||||||
context
|
context.read<LabelRepository<T>>(),
|
||||||
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
|
|
||||||
),
|
),
|
||||||
child: AddLabelFormWidget(
|
child: AddLabelFormWidget(
|
||||||
pageTitle: pageTitle,
|
pageTitle: pageTitle,
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
|
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
|
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
|
||||||
class EditLabelPage<T extends Label> extends StatelessWidget {
|
class EditLabelPage<T extends Label> extends StatelessWidget {
|
||||||
final T label;
|
final T label;
|
||||||
@@ -27,8 +28,7 @@ class EditLabelPage<T extends Label> extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit(
|
create: (context) => EditLabelCubit(
|
||||||
context
|
context.read<LabelRepository<T>>(),
|
||||||
.read<LabelRepository<Label, RepositoryState<Map<int, Label>>>>(),
|
|
||||||
),
|
),
|
||||||
child: EditLabelForm(
|
child: EditLabelForm(
|
||||||
label: label,
|
label: label,
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class AddCorrespondentPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<Correspondent>(
|
create: (context) => EditLabelCubit<Correspondent>(
|
||||||
context.read<
|
context.read<LabelRepository<Correspondent>>(),
|
||||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: AddLabelPage<Correspondent>(
|
child: AddLabelPage<Correspondent>(
|
||||||
pageTitle: Text(S.of(context).addCorrespondentPageTitle),
|
pageTitle: Text(S.of(context).addCorrespondentPageTitle),
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ class AddDocumentTypePage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<DocumentType>(
|
create: (context) => EditLabelCubit<DocumentType>(
|
||||||
context
|
context.read<LabelRepository<DocumentType>>(),
|
||||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: AddLabelPage<DocumentType>(
|
child: AddLabelPage<DocumentType>(
|
||||||
pageTitle: Text(S.of(context).addDocumentTypePageTitle),
|
pageTitle: Text(S.of(context).addDocumentTypePageTitle),
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ class AddStoragePathPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<StoragePath>(
|
create: (context) => EditLabelCubit<StoragePath>(
|
||||||
context
|
context.read<LabelRepository<StoragePath>>(),
|
||||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: AddLabelPage<StoragePath>(
|
child: AddLabelPage<StoragePath>(
|
||||||
pageTitle: Text(S.of(context).addStoragePathPageTitle),
|
pageTitle: Text(S.of(context).addStoragePathPageTitle),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class AddTagPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<Tag>(
|
create: (context) => EditLabelCubit<Tag>(
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
context.read<LabelRepository<Tag>>(),
|
||||||
),
|
),
|
||||||
child: AddLabelPage<Tag>(
|
child: AddLabelPage<Tag>(
|
||||||
pageTitle: Text(S.of(context).addTagPageTitle),
|
pageTitle: Text(S.of(context).addTagPageTitle),
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class EditCorrespondentPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<Correspondent>(
|
create: (context) => EditLabelCubit<Correspondent>(
|
||||||
context.read<
|
context.read<LabelRepository<Correspondent>>(),
|
||||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: EditLabelPage<Correspondent>(
|
child: EditLabelPage<Correspondent>(
|
||||||
label: correspondent,
|
label: correspondent,
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class EditDocumentTypePage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<DocumentType>(
|
create: (context) => EditLabelCubit<DocumentType>(
|
||||||
context
|
context.read<LabelRepository<DocumentType>>(),
|
||||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: EditLabelPage<DocumentType>(
|
child: EditLabelPage<DocumentType>(
|
||||||
label: documentType,
|
label: documentType,
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class EditStoragePathPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<StoragePath>(
|
create: (context) => EditLabelCubit<StoragePath>(
|
||||||
context
|
context.read<LabelRepository<StoragePath>>(),
|
||||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
|
||||||
),
|
),
|
||||||
child: EditLabelPage<StoragePath>(
|
child: EditLabelPage<StoragePath>(
|
||||||
label: storagePath,
|
label: storagePath,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class EditTagPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => EditLabelCubit<Tag>(
|
create: (context) => EditLabelCubit<Tag>(
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
context.read<LabelRepository<Tag>>(),
|
||||||
),
|
),
|
||||||
child: EditLabelPage<Tag>(
|
child: EditLabelPage<Tag>(
|
||||||
label: tag,
|
label: tag,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import 'package:paperless_mobile/core/translation/matching_algorithm_localizatio
|
|||||||
import 'package:paperless_mobile/core/type/types.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
|
||||||
class SubmitButtonConfig<T extends Label> {
|
class SubmitButtonConfig<T extends Label> {
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/global/constants.dart';
|
import 'package:paperless_mobile/core/global/constants.dart';
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/route_description.dart';
|
import 'package:paperless_mobile/features/home/view/route_description.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
|
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/app_drawer.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/labels/view/pages/labels_page.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
@@ -30,9 +32,10 @@ import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
|
|||||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/helpers/file_helpers.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
@@ -45,11 +48,20 @@ class HomePage extends StatefulWidget {
|
|||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
|
final DocumentScannerCubit _scannerCubit = DocumentScannerCubit();
|
||||||
|
late final InboxCubit _inboxCubit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initializeData(context);
|
_initializeData(context);
|
||||||
|
_inboxCubit = InboxCubit(
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
);
|
||||||
context.read<ConnectivityCubit>().reload();
|
context.read<ConnectivityCubit>().reload();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
_listenForReceivedFiles();
|
_listenForReceivedFiles();
|
||||||
@@ -109,7 +121,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider.value(
|
builder: (context) => BlocProvider.value(
|
||||||
value: DocumentUploadCubit(
|
value: DocumentUploadCubit(
|
||||||
localVault: context.read(),
|
|
||||||
documentApi: context.read(),
|
documentApi: context.read(),
|
||||||
tagRepository: context.read(),
|
tagRepository: context.read(),
|
||||||
correspondentRepository: context.read(),
|
correspondentRepository: context.read(),
|
||||||
@@ -137,7 +148,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
toastLength: Toast.LENGTH_LONG,
|
toastLength: Toast.LENGTH_LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e) {
|
||||||
Fluttertoast.showToast(
|
Fluttertoast.showToast(
|
||||||
msg: S.of(context).receiveSharedFilePermissionDeniedMessage,
|
msg: S.of(context).receiveSharedFilePermissionDeniedMessage,
|
||||||
toastLength: Toast.LENGTH_LONG,
|
toastLength: Toast.LENGTH_LONG,
|
||||||
@@ -145,6 +156,12 @@ class _HomePageState extends State<HomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_inboxCubit.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final destinations = [
|
final destinations = [
|
||||||
@@ -172,35 +189,38 @@ class _HomePageState extends State<HomePage> {
|
|||||||
),
|
),
|
||||||
label: S.of(context).bottomNavLabelsPageLabel,
|
label: S.of(context).bottomNavLabelsPageLabel,
|
||||||
),
|
),
|
||||||
// RouteDescription(
|
RouteDescription(
|
||||||
// icon: const Icon(Icons.inbox_outlined),
|
icon: const Icon(Icons.inbox_outlined),
|
||||||
// selectedIcon: Icon(
|
selectedIcon: Icon(
|
||||||
// Icons.inbox,
|
Icons.inbox,
|
||||||
// color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
// ),
|
),
|
||||||
// label: S.of(context).bottomNavInboxPageLabel,
|
label: S.of(context).bottomNavInboxPageLabel,
|
||||||
// ),
|
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
|
||||||
// RouteDescription(
|
bloc: _inboxCubit,
|
||||||
// icon: const Icon(Icons.settings_outlined),
|
builder: (context, state) {
|
||||||
// selectedIcon: Icon(
|
if (state.itemsInInboxCount > 0) {
|
||||||
// Icons.settings,
|
return Badge.count(
|
||||||
// color: Theme.of(context).colorScheme.primary,
|
count: state.itemsInInboxCount,
|
||||||
// ),
|
child: icon,
|
||||||
// label: S.of(context).appDrawerSettingsLabel,
|
);
|
||||||
// ),
|
}
|
||||||
|
return icon;
|
||||||
|
},
|
||||||
|
)),
|
||||||
];
|
];
|
||||||
final routes = <Widget>[
|
final routes = <Widget>[
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => DocumentsCubit(
|
create: (context) => DocumentsCubit(
|
||||||
context.read<PaperlessDocumentsApi>(),
|
context.read(),
|
||||||
context.read<SavedViewRepository>(),
|
context.read(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => SavedViewCubit(
|
create: (context) => SavedViewCubit(
|
||||||
context.read<SavedViewRepository>(),
|
context.read(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -210,7 +230,28 @@ class _HomePageState extends State<HomePage> {
|
|||||||
value: _scannerCubit,
|
value: _scannerCubit,
|
||||||
child: const ScannerPage(),
|
child: const ScannerPage(),
|
||||||
),
|
),
|
||||||
const LabelsPage(),
|
MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => LabelCubit<Correspondent>(context.read()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => LabelCubit<DocumentType>(context.read()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => LabelCubit<Tag>(context.read()),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => LabelCubit<StoragePath>(context.read()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const LabelsPage(),
|
||||||
|
),
|
||||||
|
BlocProvider.value(
|
||||||
|
value: _inboxCubit,
|
||||||
|
child: const InboxPage(),
|
||||||
|
),
|
||||||
|
// const SettingsPage(),
|
||||||
];
|
];
|
||||||
return MultiBlocListener(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
@@ -237,8 +278,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
builder: (context, sizingInformation) {
|
builder: (context, sizingInformation) {
|
||||||
if (!sizingInformation.isMobile) {
|
if (!sizingInformation.isMobile) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
|
||||||
drawer: const AppDrawer(),
|
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
NavigationRail(
|
NavigationRail(
|
||||||
@@ -258,15 +297,14 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
|
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
selectedIndex: _currentIndex,
|
selectedIndex: _currentIndex,
|
||||||
onDestinationSelected: _onNavigationChanged,
|
onDestinationSelected: _onNavigationChanged,
|
||||||
destinations:
|
destinations:
|
||||||
destinations.map((e) => e.toNavigationDestination()).toList(),
|
destinations.map((e) => e.toNavigationDestination()).toList(),
|
||||||
),
|
),
|
||||||
drawer: const AppDrawer(),
|
|
||||||
body: routes[_currentIndex],
|
body: routes[_currentIndex],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -282,16 +320,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
void _initializeData(BuildContext context) {
|
void _initializeData(BuildContext context) {
|
||||||
try {
|
try {
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>().findAll();
|
context.read<LabelRepository<Tag>>().findAll();
|
||||||
context
|
context.read<LabelRepository<Correspondent>>().findAll();
|
||||||
.read<LabelRepository<Correspondent, CorrespondentRepositoryState>>()
|
context.read<LabelRepository<DocumentType>>().findAll();
|
||||||
.findAll();
|
context.read<LabelRepository<StoragePath>>().findAll();
|
||||||
context
|
|
||||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>()
|
|
||||||
.findAll();
|
|
||||||
context
|
|
||||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>()
|
|
||||||
.findAll();
|
|
||||||
context.read<SavedViewRepository>().findAll();
|
context.read<SavedViewRepository>().findAll();
|
||||||
context.read<PaperlessServerInformationCubit>().updateInformtion();
|
context.read<PaperlessServerInformationCubit>().updateInformtion();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -4,18 +4,20 @@ class RouteDescription {
|
|||||||
final String label;
|
final String label;
|
||||||
final Icon icon;
|
final Icon icon;
|
||||||
final Icon selectedIcon;
|
final Icon selectedIcon;
|
||||||
|
final Widget Function(Widget icon)? badgeBuilder;
|
||||||
|
|
||||||
RouteDescription({
|
RouteDescription({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.selectedIcon,
|
required this.selectedIcon,
|
||||||
|
this.badgeBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
NavigationDestination toNavigationDestination() {
|
NavigationDestination toNavigationDestination() {
|
||||||
return NavigationDestination(
|
return NavigationDestination(
|
||||||
label: label,
|
label: label,
|
||||||
icon: icon,
|
icon: badgeBuilder?.call(icon) ?? icon,
|
||||||
selectedIcon: selectedIcon,
|
selectedIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +32,8 @@ class RouteDescription {
|
|||||||
BottomNavigationBarItem toBottomNavigationBarItem() {
|
BottomNavigationBarItem toBottomNavigationBarItem() {
|
||||||
return BottomNavigationBarItem(
|
return BottomNavigationBarItem(
|
||||||
label: label,
|
label: label,
|
||||||
icon: icon,
|
icon: badgeBuilder?.call(icon) ?? icon,
|
||||||
activeIcon: selectedIcon,
|
activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
320
lib/features/home/view/widget/_app_drawer.dart
Normal file
320
lib/features/home/view/widget/_app_drawer.dart
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
// import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
// import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
// import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
// import 'package:paperless_api/paperless_api.dart';
|
||||||
|
// import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
|
// import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
|
// import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
// import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.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/extensions/flutter_extensions.dart';
|
||||||
|
// import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
|
||||||
|
// import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
|
||||||
|
// import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
// import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
|
// import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
// import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
// import 'package:paperless_mobile/constants.dart';
|
||||||
|
// import 'package:url_launcher/link.dart';
|
||||||
|
// import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
// class AppDrawer extends StatefulWidget {
|
||||||
|
// final VoidCallback? afterInboxClosed;
|
||||||
|
|
||||||
|
// const AppDrawer({Key? key, this.afterInboxClosed}) : super(key: key);
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// State<AppDrawer> createState() => _AppDrawerState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // enum NavigationDestinations {
|
||||||
|
// // inbox,
|
||||||
|
// // settings,
|
||||||
|
// // reportBug,
|
||||||
|
// // about,
|
||||||
|
// // logout;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// class _AppDrawerState extends State<AppDrawer> {
|
||||||
|
// @override
|
||||||
|
// void initState() {
|
||||||
|
// super.initState();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// final listtTileShape = RoundedRectangleBorder(
|
||||||
|
// borderRadius: BorderRadius.circular(32),
|
||||||
|
// );
|
||||||
|
// // return NavigationDrawer(
|
||||||
|
// // selectedIndex: -1,
|
||||||
|
// // children: [
|
||||||
|
// // Text(
|
||||||
|
// // "",
|
||||||
|
// // style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
// // ).padded(16),
|
||||||
|
// // NavigationDrawerDestination(
|
||||||
|
// // icon: const Icon(Icons.inbox),
|
||||||
|
// // label: Text(S.of(context).bottomNavInboxPageLabel),
|
||||||
|
// // ),
|
||||||
|
// // NavigationDrawerDestination(
|
||||||
|
// // icon: const Icon(Icons.settings),
|
||||||
|
// // label: Text(S.of(context).appDrawerSettingsLabel),
|
||||||
|
// // ),
|
||||||
|
// // const Divider(
|
||||||
|
// // indent: 16,
|
||||||
|
// // ),
|
||||||
|
// // NavigationDrawerDestination(
|
||||||
|
// // icon: const Icon(Icons.bug_report),
|
||||||
|
// // label: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
|
// // ),
|
||||||
|
// // NavigationDrawerDestination(
|
||||||
|
// // icon: const Icon(Icons.info_outline),
|
||||||
|
// // label: Text(S.of(context).appDrawerAboutLabel),
|
||||||
|
// // ),
|
||||||
|
// // ],
|
||||||
|
// // onDestinationSelected: (idx) {
|
||||||
|
// // final val = NavigationDestinations.values[idx - 1];
|
||||||
|
// // switch (val) {
|
||||||
|
// // case NavigationDestinations.inbox:
|
||||||
|
// // _onOpenInbox();
|
||||||
|
// // break;
|
||||||
|
// // case NavigationDestinations.settings:
|
||||||
|
// // _onOpenSettings();
|
||||||
|
// // break;
|
||||||
|
// // case NavigationDestinations.reportBug:
|
||||||
|
// // launchUrlString(
|
||||||
|
// // 'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||||
|
// // );
|
||||||
|
// // break;
|
||||||
|
// // case NavigationDestinations.about:
|
||||||
|
// // _onShowAboutDialog();
|
||||||
|
// // break;
|
||||||
|
// // case NavigationDestinations.logout:
|
||||||
|
// // _onLogout();
|
||||||
|
// // break;
|
||||||
|
// // }
|
||||||
|
// // },
|
||||||
|
// // );
|
||||||
|
// return SafeArea(
|
||||||
|
// top: true,
|
||||||
|
// child: ClipRRect(
|
||||||
|
// borderRadius: const BorderRadius.only(
|
||||||
|
// topRight: Radius.circular(16.0),
|
||||||
|
// bottomRight: Radius.circular(16.0),
|
||||||
|
// ),
|
||||||
|
// child: Drawer(
|
||||||
|
// shape: const RoundedRectangleBorder(
|
||||||
|
// borderRadius: BorderRadius.only(
|
||||||
|
// topRight: Radius.circular(16.0),
|
||||||
|
// bottomRight: Radius.circular(16.0),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// child: ListView(
|
||||||
|
// children: [
|
||||||
|
// DrawerHeader(
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
// ),
|
||||||
|
// padding: const EdgeInsets.only(
|
||||||
|
// top: 8,
|
||||||
|
// left: 8,
|
||||||
|
// bottom: 0,
|
||||||
|
// right: 8,
|
||||||
|
// ),
|
||||||
|
// child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
// children: [
|
||||||
|
// Row(
|
||||||
|
// children: [
|
||||||
|
// Image.asset(
|
||||||
|
// 'assets/logos/paperless_logo_white.png',
|
||||||
|
// height: 32,
|
||||||
|
// width: 32,
|
||||||
|
// color:
|
||||||
|
// Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
// ).paddedOnly(right: 8.0),
|
||||||
|
// Text(
|
||||||
|
// S.of(context).appTitleText,
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .textTheme
|
||||||
|
// .headlineSmall
|
||||||
|
// ?.copyWith(
|
||||||
|
// color: Theme.of(context)
|
||||||
|
// .colorScheme
|
||||||
|
// .onPrimaryContainer,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// Align(
|
||||||
|
// alignment: Alignment.bottomRight,
|
||||||
|
// child: BlocBuilder<PaperlessServerInformationCubit,
|
||||||
|
// PaperlessServerInformationState>(
|
||||||
|
// builder: (context, state) {
|
||||||
|
// if (!state.isLoaded) {
|
||||||
|
// return Container();
|
||||||
|
// }
|
||||||
|
// final info = state.information!;
|
||||||
|
// return Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
// children: [
|
||||||
|
// ListTile(
|
||||||
|
// contentPadding: EdgeInsets.zero,
|
||||||
|
// dense: true,
|
||||||
|
// title: Text(
|
||||||
|
// S.of(context).appDrawerHeaderLoggedInAsText +
|
||||||
|
// (info.username ?? '?'),
|
||||||
|
// style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
// textAlign: TextAlign.end,
|
||||||
|
// maxLines: 1,
|
||||||
|
// ),
|
||||||
|
// subtitle: Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
// children: [
|
||||||
|
// Text(
|
||||||
|
// state.information!.host ?? '',
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .textTheme
|
||||||
|
// .bodyMedium,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
// textAlign: TextAlign.end,
|
||||||
|
// maxLines: 1,
|
||||||
|
// ),
|
||||||
|
// Text(
|
||||||
|
// '${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})',
|
||||||
|
// style:
|
||||||
|
// Theme.of(context).textTheme.bodySmall,
|
||||||
|
// overflow: TextOverflow.ellipsis,
|
||||||
|
// textAlign: TextAlign.end,
|
||||||
|
// maxLines: 1,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// isThreeLine: true,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ...[
|
||||||
|
// ListTile(
|
||||||
|
// title: Text(S.of(context).bottomNavInboxPageLabel),
|
||||||
|
// leading: const Icon(Icons.inbox),
|
||||||
|
// onTap: () => _onOpenInbox(),
|
||||||
|
// shape: listtTileShape,
|
||||||
|
// ),
|
||||||
|
// ListTile(
|
||||||
|
// leading: const Icon(Icons.settings),
|
||||||
|
// shape: listtTileShape,
|
||||||
|
// title: Text(
|
||||||
|
// S.of(context).appDrawerSettingsLabel,
|
||||||
|
// ),
|
||||||
|
// onTap: () => Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => BlocProvider.value(
|
||||||
|
// value: context.read<ApplicationSettingsCubit>(),
|
||||||
|
// child: const SettingsPage(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const Divider(
|
||||||
|
// indent: 16,
|
||||||
|
// endIndent: 16,
|
||||||
|
// ),
|
||||||
|
// ListTile(
|
||||||
|
// leading: const Icon(Icons.bug_report),
|
||||||
|
// title: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
|
// onTap: () {
|
||||||
|
// launchUrlString(
|
||||||
|
// 'https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
|
// },
|
||||||
|
// shape: listtTileShape,
|
||||||
|
// ),
|
||||||
|
// ListTile(
|
||||||
|
// title: Text(S.of(context).appDrawerAboutLabel),
|
||||||
|
// leading: Icon(Icons.info_outline_rounded),
|
||||||
|
// onTap: _onShowAboutDialog,
|
||||||
|
// shape: listtTileShape,
|
||||||
|
// ),
|
||||||
|
// ListTile(
|
||||||
|
// leading: const Icon(Icons.logout),
|
||||||
|
// title: Text(S.of(context).appDrawerLogoutLabel),
|
||||||
|
// shape: listtTileShape,
|
||||||
|
// onTap: () {
|
||||||
|
// _onLogout();
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// void _onLogout() async {
|
||||||
|
// 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<SavedViewRepository>().clear();
|
||||||
|
// await HydratedBloc.storage.clear();
|
||||||
|
// } on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
// showErrorMessage(context, error, stackTrace);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Future<void> _onOpenInbox() async {
|
||||||
|
// await Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (_) => LabelRepositoriesProvider(
|
||||||
|
// child: BlocProvider(
|
||||||
|
// create: (context) => InboxCubit(
|
||||||
|
// context.read(),
|
||||||
|
// context.read(),
|
||||||
|
// context.read(),
|
||||||
|
// context.read(),
|
||||||
|
// )..initializeInbox(),
|
||||||
|
// child: const InboxPage(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// widget.afterInboxClosed?.call();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// void _onOpenSettings() {
|
||||||
|
// Navigator.of(context).push(
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder: (context) => BlocProvider.value(
|
||||||
|
// value: context.read<ApplicationSettingsCubit>(),
|
||||||
|
// child: const SettingsPage(),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// void _onShowAboutDialog() {}
|
||||||
|
// }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user